15 ms·
A trick to have arbitrary infix operators in Python
- atorodius 5y agoPretty neat trick. I often wish for a built in way to do a bit more operator magic, but I can see why it would be bad to have it.
- joe_the_user 5y agoPeople are used to math infix operators and can use them efficiently. 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.
- yccs27 5y agoYeah, relying on operator precedence makes for some of the most infuriating to read code.
- abrudz 5y agoYeah, infix doesn't go well with that. Even traditional mathematical notation ends up being ambiguous because of it. However, if all infix operators are given equal precedence, it all becomes very neat. See APL (https://apl.wiki https://apl.wiki) for an example of that.
- maximus-decimus 5y agoAt the same time Q (which is based on apl) only allows built in functions to be infix because otherwise your code gets unreadable Especially if you also have prefix notation. 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.
- mkl 5y agoCode written by people who don't know operator precedence and so put brackets around absolutely everything is significantly harder to read IME. I've seen students end up with "))))))" at the end of a line. Brackets are better as a signal that something unusual's happening.
- ogogmad 5y agoSometimes it's nice to be able to do piping: 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.
- andreareina 5y agoAn interesting feature of D is Uniform Function Call Syntax (UFCS) where any function with first parameter of type T can be called as a method on T: https://dlang.org/spec/function.html#pseudo-member https://dlang.org/spec/function.html#pseudo-member
- xigoi 5y agoOther languages I know of that have this feature are Nim, Koka and VimScript.
- codethief 5y agoThat is indeed an interesting feature! I wish more languages implemented it! 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 ".").
- runevault 5y agoI love |> in f# because of this type of thing, piping operations together instead of nesting or the like just reads so much cleaner. And yes in cases where they are all methods on the type you can have the function return self and then do value.Func1().func2().func3().etc() but only if they are methods on the class. 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).
- smaudet 5y ago> 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. 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/ 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.
- yccs27 5y agoThis is a super neat hack. Unfortunately, this kind of custom notation can make a project much less readable to outsiders who are only familiar with the base language. Probably the reason why Python disallows custom operators. Incidentally, the Wolfram Language has a similar notation for arbitrary functions: a~Plus~b.
- srcreigh 5y agoRacket does too. (when (0 . < . x) (displayln "positive")) Although in Racket you could also add your own syntax.
- anon_123g987 5y agoInfix operator in R: > '%mult%' <- function(a,b) a*b > 3 %mult% 5 [1] 15 Using infix operators as functions: > '*'(3,5) [1] 15
- bhrgunatha 5y agoI've always wondered why that was added. It feels to me against the idiom (argot?) of any lisp. It's not composable either. (0 . < . x . < . 12) read-syntax: illegal use of `.` Typing those extra '.'s gets annoying very quickly too.
- srcreigh 5y agoMy guess is Racket/PLT scheme's history as a language for teaching programming to students. 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.
- kazinator 5y agoMacLisp had an extended dot notation similar to this for hunks: https://www.maclisp.info/pitmanual/hunks.html https://www.maclisp.info/pitmanual/hunks.html
- assbuttbuttass 5y agoThis is pretty neat, but I always find I prefer the simplicity and extensibility of lisp's syntax, despite the difference to math notation. For example, the author's example of add(add(add(5,6),7),8)... is just (+ 5 6 7 8 ...). Haskell supports custom infix operators, but you end up having an unreadable soup of <* <$> <$ >>=
- toomanydoubts 5y ago>but you end up having an unreadable soup of <* <$> <$ >>= True to some extent. After learning haskell, I find these operators very readable actually.
- dataangel 5y agoHow do you Google them? As a Haskell newbie this was a big problem for me.
- lalaithion 5y agohttps://hoogle.haskell.org/?hoogle=%3C%24%3E https://hoogle.haskell.org/?hoogle=%3C%24%3E
- bobbylarrybobby 5y agoOften, by type signature using Hoogle
- yoyohello13 5y agoIn ghci you can do: Prelude> :doc <$> and Prelude> :t (<$>)
- pizza 5y agohoogle or hoogle+
- d0mine 5y agoPython supports add(5,6,7,8, *numbers) syntax. 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
- barrenko 5y agoJust stop.
- animal_spirits 5y ago(2012)
- powersnail 5y agoThis is hilarious. Probably nobody is going to use this in production, (infix operator is a bad idea outside of math operations, in my opinion), but it's interesting to see how much you can coerce a language into almost a different language altogether. 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_enjarify_in_go_and_rust/ https://www.reddit.com/r/rust/comments/5penft/parallelizing_...
- UncombedCoconut 5y agoFWIW: A pattern along these lines is in B of A's Quartz, allowing the construction of table filters like "Where('colname') <<inlist>> ['cat', 'dog']".
- dataflow 5y ago> infix operator is a bad idea outside of math operations, in my opinion What about string + string?
- powersnail 5y agoI was too rash with my comment. Indeed, string + string is also quite intuitive.
- dataflow 5y agoHaha no worries. I think there are more cases outside (what ordinary folks would call) "math", where infix operators make sense; they're just not as common or obvious. Like one example is when you want to build expression trees; you're not actually doing math there, but merely creating a representation of the very expressions you're writing. Another one is &, |, ^, ~ for enum flags (which you can define bitwise so it's "math" in that sense, but which you might want more domain-specific logic for). Another one is overloading <, =, > for arbitrary objects. And so on. I think the real rule is "it should be intuitive rather than confusing". :-)
- naniwaduni 5y ago
- dragonwriter 5y ago> As you may already know, there are 3 kinds of operators calling-notations: prefix (+ 3 5), infix (3 + 5), and postfix (3 5 +) 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.
- bradrn 5y agoIt goes further. Agda allows mixfix operators [0], which allows expressions to be arbitrarily interleaved with names and symbols, e.g.: 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-operators.html https://agda.readthedocs.io/en/v2.6.0.1/language/mixfix-oper...
- agumonkey 5y agoHey that's lifted from Smalltalk isn't it ?
- dragonwriter 5y agoSmalltalk has functions with keyword arguments that look kind of like mixfix operators.
- travisgriggs 5y agoI miss keyword syntax. It has its warts, but I liked the clarity better than the whole funarg() list mess.
- agumonkey 5y agofunny, googling mixfix taught me racket also had something for it https://docs.racket-lang.org/mixfix/ https://docs.racket-lang.org/mixfix/
- 5y ago
- raymondh 5y agoThis is a pretty cool recipe. The 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#operator-precedence https://docs.python.org/3/reference/expressions.html#operato...
- CornCobs 5y agoNice! It just so happens that %% is used to denote infix operators in R (which can have arbitrary names). Like the %in% operator in base, or the famous pipe operator %>% from magrittr
- kragen 5y agoOoh, I had no idea. That might swing the balance in favor of %beside%, since R is super popular.
- lostmsu 5y agoI think this would work in many languages with operator overloading. Same thing in C#: https://gist.github.com/lostmsu/6922f6e1f5b4f07c88358e92d6e8b312 https://gist.github.com/lostmsu/6922f6e1f5b4f07c88358e92d6e8... Not sure yet if it is possible to make this work for generic operators.
- eyegor 5y agoC# does not allow generic custom operators, F# does, C doesn't, C++ does, several lisps do, but it's certainly not a common language feature. In C# you can overload most of the normal operators, but you can't, for example, define your own "~==" or overload "+=" to be anything other than "= self +"
- lostmsu 5y agoBy generic operators I did not mean custom operator tokens, but operators, that take type parameters. Or I did not understand the relevance of your comment to my pondering.
- amelius 5y agoWhat's the performance penalty?
- kragen 5y agoPretty bad, but in a lot of cases you can use this to set up some kind of dataflow graph you use for long enough to amortize the overhead.
- zomglings 5y agoApache Beam works this way. Code sample: https://github.com/apache/beam/blob/master/examples/multi-language/python/addprefix.py#L56 https://github.com/apache/beam/blob/master/examples/multi-la...
- kragen 5y agoYeah, it's a very widely used technique to use slow scripting languages (with or without operator overloading) to set up some kind of graph describing a computation that takes a lot longer to run than the scripting-language statement takes to interpret. In addition to Beam, we could mention the Unix shell, Pandas, Numpy, TensorFlow, SQL, SQLAlchemy, APL, Lush, PIL, CSS, some Linux system calls like sendfile(), maybe Apache SPARK, etc.
- bayesian_horse 5y agoYou wouldn't use it in any situation where performance is important...
- jonnycomputer 5y agoVery cool, but its the kind of clever stuff I hate to see in code, unless there is a really compelling use case. Maybe for piping with pandas similar to dplyr
- bayesian_horse 5y agoSee 'https://github.com/machow/siuba https://github.com/machow/siuba'.
- nneonneo 5y agoOne of my favorite (ab)uses of operators is the "q" package for debugging: https://pypi.org/project/q/ https://pypi.org/project/q/ 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.
- Siira 5y agoI use icecream: `ic(debug_expr)`. Not as concise, of course.
- jaytaylor 5y agoPlease read the comment below in the gentlest, friendliest tone possible, because my goal is to considerately inform and persuade you, dear writer :) 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 https://github.com/gruns/icecream Example: from icecream import ic def foo(i): return i + 333 ic(foo(123)) Prints ic| foo(123): 456
- jounker 5y agoThank you! I’ve been trying to remember this package’s name.
- ziml77 5y ago
- aflag 5y agoIt’s a neat trick, but I feel like this can break in surprising ways. After all, you’re thinking of |fun| as a single thing, but in reality there are a couple calls in there and the precedence of something like 3 % 2 |add| 5 - 2 could be unintuitive.
- andreareina 5y agoSame thing happens in pandas when composing conditionals in a selection operation. You need to use `|` instead of `or`, but its precedence is low so you need to add parens: df[(df["col1"] == foo) | (df["col2"] == bar)]
- nojs 5y ago> Now imagine I have a function, add(x,y), and I have an expression like add(add(add(5,6),7),8)... wouldn’t it be cool if I could use infix notation here? Cool trick, but you can also just use def add(*args).
- kragen 5y agoI can't believe I never knew about this. This is super cool, and although yes it's potentially subject to readability problems, it allows you enough freedom to design your desired notation that you might be able to come up with something that's more readable than standard Python for a given embedded DSL rather than less, in particular for associative operators. And it's way easier to search the documentation for than non-alphabetic operators. Consider Tk-style page or GUI layout, as in http://canonical.org/~kragen/sw/dev3/alglayout.py http://canonical.org/~kragen/sw/dev3/alglayout.py (in OCaml I used `:` and `..` http://canonical.org/~kragen/sw/dev3/alglayout.ml 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 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 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 http://canonical.org/~kragen/synthgramelodia and http://canonical.org/~kragen/sw/aspmisc/gramelodia.py 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.
- vasili111 5y agoI whish Python had piping so we do not need to do something like that.
- CornCobs 5y agoI've never seen this name-in-operator syntax before. Does this work with all 2-character operators? Where is this documented and what are the usecases?
- dragonwriter 5y agoIt's a hack using a single character binary operator and the way it's implemented for the type that serves as the main “operator” on between the paired use. > 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.
- CornCobs 5y agoOHHH. Looking at the Infix class carefully I realize it's not actually |add| as a single operator but using the right or __ror__ to partially apply the function, then using the left or __or__ to fully apply it. Wrapping it up in a decorator class is really neat!
- keeganpoppen 5y agowhat a magnificent trick! feels even more subversive in a "no fun allowed" language / ecosystem as well. i doubt i'll ever have a good reason to use it, but that doesn't mean that i will never use it (muahaha).
- prionassembly 5y agokey_value = Infix(lambda u,v: dict(zip(list(u),list(v)))) Let's goooo.
- cttet 5y agoWe could have sacrilegious Python version that put up all the stuffs lol. Such as multiline lambda :p here: def begin(\*args): return args[-1] begin( func := lambda x, y: begin( z := int(input()), x + y + z ), func(1, 2) )
- pizza 5y agoThat's awesome! My personal opinion; lambdas in Python are really limited relative to their potential. We should be better able to exploit the contrast between "a lambda is an expression" and "a def function is a statement." 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.
- bhawks 5y agoThat's gross. I love it. 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.
- zomglings 5y agoThis is indeed a clever and nice trick. 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.
- smaudet 5y ago> 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.
- carapace 5y agoI recall someone doing this using the shift operators <<, >>: a <<op>> b <<op>> c
- bayesian_horse 5y agoThat's pretty nifty... 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...
- gpderetta 5y agoThis trick sometimes pops up in C++ as well, although I never seen it used in production. I think the earliest use I can remember is with the experimental FC++ library[1], circa 2001. [1] https://yanniss.github.io/fc++/ https://yanniss.github.io/fc++/
- steerablesafe 5y ago> although I never seen it used in production Well, of course boost has it, they use `<` and `>`: [1] https://www.boost.org/doc/libs/1_78_0/libs/hof/doc/html/include/boost/hof/infix.html https://www.boost.org/doc/libs/1_78_0/libs/hof/doc/html/incl...
- HerrMonnezza 5y agoThere's a few libraries on PyPI implementing this idea: - https://github.com/borntyping/python-infix https://github.com/borntyping/python-infix (https://pypi.org/project/infix/ https://pypi.org/project/infix/) - https://pypi.org/project/infixed/ https://pypi.org/project/infixed/ - https://pypi.org/project/betwixt/ https://pypi.org/project/betwixt/ (Diclaimer: author here)
- globular-toast 5y agoThis is why we love Python! Yeah, I know we should do boring stuff for production and not get creative, but creativity is the real joy. > 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.
- gilch 5y agoYou only need the parens when the arity can vary. It doesn't have to be binary. Red and REBOL are prefix languages like Lisp, but without the parentheses, and their calls can have more than 2 arguments. You do have to know the arity of each function to make a parse tree, so the arity of any given function is fixed.
- account42 5y agoHah, I did this in C++ once as a joke for calling std::swap() as a <swap> b;
- draegtun 5y agoI remember seeing the original Activestate post and porting it to Perl. But it was 12 years ago so took me a while to find it :) https://gist.github.com/draegtun/540493 https://gist.github.com/draegtun/540493