Jay Taylor's notes

back to listing index

A trick to have arbitrary infix operators in Python | Hacker News

[web search]
Original source (news.ycombinator.com)
Tags: python cool-trick news.ycombinator.com
Clipped on: 2022-03-09

Image (Asset 1/2) alt=
Image (Asset 2/2) alt=
One of my favorite (ab)uses of operators is the "q" package for debugging: 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.


Just last week I was thinking that it would be great if Python had a way to print both the name and value of a variable without having to repeat the variable name. I checked the formatting mini-language spec and when I didn't see it there I assumed it didn't have it. I'm glad to know now that it does have that feature!


There are several versions of the "the formatting mini-language" and the documentation that Google return first is for an old one which didn't have this feature :-(


Aw, it's such a delight to open Hacker News and see this at the top of the comments! Glad you're enjoying q.

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.


I use icecream: ic(debug_expr). Not as concise, of course.

*

Please 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

Example:

    from icecream import ic

    def foo(i):
        return i + 333
    
    ic(foo(123))
Prints

    ic| foo(123): 456


Software library naming has always been terrible and mostly un-searchable, so links definitely help


For people who know how to use search engines, PP's version may be better since it wastes less vertical space on HN.


Thank you! I’ve been trying to remember this package’s name.


This 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#operato...


Nice! 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


Ooh, I had no idea. That might swing the balance in favor of %beside%, since R is super popular.


> 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.


It 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-oper...


Hey that's lifted from Smalltalk isn't it ?


Smalltalk has functions with keyword arguments that look kind of like mixfix operators.


I miss keyword syntax. It has its warts, but I liked the clarity better than the whole funarg() list mess.


funny, googling mixfix taught me racket also had something for it https://docs.racket-lang.org/mixfix/


There's also mixfix where the operators are interleaved between the arguments, of which there are more than two. The well-known one is C's conditional operator:

    a ? b : c
This is a single operation ?: that takes operands a, b, and c.


Except that the ternary operator in C and C++ is not really an operator but a control flow construct.


It's an operator because it's functional (although often with side effects): three expressions go in and one expression goes out. Control flow constructs deal with statements instead.


It's an operator in the syntactic sense in the same way that "=", ".", and "||" are all operators.


Some examples https://docs.raku.org/language/operators#Operator_classifica...

ps: we're going back to APL it seems


This 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_...


> infix operator is a bad idea outside of math operations, in my opinion

What about string + string?


I was too rash with my comment. Indeed, string + string is also quite intuitive.


Haha 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". :-)


It's always been a bad idea, because concatenation is a natural product, not a sum. We should write string * string.


> concatenation is a natural product, not a sum

Huh? Why?


Because monoids, abstract algebra, etc.

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.)


I think when mathematicians call an operation denoted by juxtaposition a "product", that's usually because it's denoted by juxtaposition for some independent reason and they're drawing the syntactic analogy with the numerical product, and there isn't any deeper connection with multiplication than that. Independent reasons for using juxtaposition might include:

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.


I think multiplication for string concatenation makes more sense than just "its juxtaposition so we call it multiplication". If you look at type theory, then a tuple of type (A, B) is called the product type of A and B. This is because there are #A times #B elements in the new type. A Sum type, in type theory is a tagged union. It is an element of Either A or B. (Like a C union with an extra tag saying which type it contains). This is a sum because it contains #A plus #B elements.

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.


> "addition" is generally defined as a commutative operation, which this isn't.

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?


I'm not saying it can't (you'll find occasional counterexamples if you Google), I'm just saying that's the general convention. I don't know why the math community chose this convention, but I know it makes sense to me in English: remember, multiplication is repeated addition. The multiplier doesn't serve the same purpose as the multiplicand; their meanings are fundamentally unrelated. Think of an example. If you have "two rectangles", it's pretty easy to understand what that means, but "rectangle twos" doesn't make any sense (or at least doesn't mean the same thing...).


> The multiplier doesn't serve the same purpose as the multiplicand

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).


> because if "A concat B" makes sense, then "B concat A" necessarily makes sense as well

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.


You are just reiterating the fact that concatenation is not commutative, to which I will reiterate my earlier response: If multiplication can be sometimes commutative and sometimes non-commutative (and it can, and it is) then why cannot addition be sometimes commutative and sometimes non-commutative?


> If multiplication can be sometimes commutative and sometimes non-commutative (and it can, and it is) then why cannot addition be sometimes commutative and sometimes non-commutative?

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).


> addition in plain English is always commutative

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?


Make those two words mean something instead of literally giving them strings, and see what they tell you. Ask them if two apples and an orange are the same thing as two oranges and an apple. Put literally any objects and ask them if the sums is the same. I guarantee you far greater than 50% of the things you come up with will result in "same" as the answer.

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 don't know why you're arguing with me back and forth like this.

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?


Here is an interesting discussion from the julia language where string concatenation is indeed the * operator:

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.


Another argument is in regular expressions, where you can see that concatenation distributes over alternation: A(B|C) is the same regex as AB|AC.


Huh, neat!. Especially since alternation is commutative. So this is a ring-like structure! We have 'identity' if you allow a pattern that matches nothing. No inverse elements though. Not sure what the name of this ring-like thing is. But pretty certain it is named.


I guess, but that ship sailed already.


not quite, since a + b != b + a feels a bit weird.


I think you missed the other thread... https://news.ycombinator.com/item?id=30081517


(a, b) would make more sense if then. Jokes aside, + also quite break the reading flows of the string such as

user_name + ": " + attr1 + "| " + attr2 + "n"

is more difficult to parse by eyeball for me then

concat(user_name, ": ", attr1, "| ", attr2, "n")


I mean sure, but the fact that you had to write 6 strings to illustrate this should tell you something, right?

This

  user_name + ": " + attr1
is easier to eyeball than

  concat(user_name, ": ", attr1)


To me they are similar though. I used Python quite a lot, for string concatenation, I mostly use fstrings. + is used mostly in the += case.


FWIW: A pattern along these lines is in B of A's Quartz, allowing the construction of table filters like "Where('colname') <<inlist>> ['cat', 'dog']".


I can see how it could be useful in actually increasing readability. Just as always, use with caution.


This 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.


I wouldn't be totally surprised if the reason Python disallows custom operators is because it either requires a lot more context in the parser or a lot of care in how they're used (or a hack re-using existing operators like TFA). Neither option is without its costs. Languages that have them usually opt for the former.

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.


Racket does too.

    (when (0 . < . x)
         (displayln "positive"))
Although in Racket you could also add your own syntax.


Infix operator in R:

  > '%mult%' <- function(a,b) a*b
  > 3 %mult% 5
  [1] 15
Using infix operators as functions:

  > '*'(3,5)
  [1] 15


I just assumed Racket supports curly infix notation, but looking just now I see that you need to use the infix pragma and wrap things with @{}

I 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))


FWIW a user can add this syntax himself to a language if he needs to.

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.


I'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.


My 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.


It was an experiment - I think the general consensus is that it was a bad idea.

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?)


MacLisp had an extended dot notation similar to this for hunks:

https://www.maclisp.info/pitmanual/hunks.html



That'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.


I recall someone doing this using the shift operators <<, >>:

    a <<op>> b <<op>> c


This 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 <* <$> <$ >>=


>but you end up having an unreadable soup of <* <$> <$ >>=

True to some extent. After learning haskell, I find these operators very readable actually.


How do you Google them? As a Haskell newbie this was a big problem for me.



Often, by type signature using Hoogle


In ghci you can do:

Prelude> :doc <$>

and

Prelude> :t (<$>)


hoogle or hoogle+


Haskell also supports using any old function as an infix function by putting backtics around it, for instance (x div y). It’s a nifty feature that can improve code readability (especially in more mathematical contexts) when used well, I wish more languages had it.


Python 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


This 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.


> 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.


That'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...


I 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 (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.


This 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.


You 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.


People 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.


Sometimes 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.


An 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


That 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 ".").


Other languages I know of that have this feature are Nim, Koka and VimScript.


I 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).


> but only if they are methods on the class

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.


Sure, you can fake it to get to the same place. And I've abused extension methods a lot, including on interfaces before c# made default implementations on interfaces a more acceptable thing with c#... 9 I think?


F# does allow custom infix operators, but of course prefix + piping is generally cleaner. Only time I've used custom infix are for implementing a "null coalesce" operator and for overloading math ops for vectors and matrices. The null coalescing is generally only used for code that interops with c# since f# offers non null guarantees for most types.


I can see wanting null coalesce in interop scenarios. I know I use it a lot in C#.


> I suppose the piping operator is infix.

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...)


I think

    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)))


Yes, true! Same point I think though? I just erroneously gave object a different type to dothis/dothat.


Agreed!


Is it possible to do this in Python, such as:

instead of: len(my_list)

to do: my_list | len

of course you can still do: my_list.__len__() but it's ugly


Yeah, relying on operator precedence makes for some of the most infuriating to read code.


Yeah, 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) for an example of that.


At 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.


Code 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.


> 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/).

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.


I 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


This 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++/


> 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/incl...


It’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.


Same 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)]


Very 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



Pretty 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.


Hah, I did this in C++ once as a joke for calling std::swap() as

  a <swap> b;


what 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).


We 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)
  )


That'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.


    key_value = Infix(lambda u,v: dict(zip(list(u),list(v))))
Let's goooo.


> 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).


I'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?


It'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.


OHHH. 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!


What's the performance penalty?


Pretty 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.


Apache Beam works this way. Code sample: https://github.com/apache/beam/blob/master/examples/multi-la...


Yeah, 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.


You wouldn't use it in any situation where performance is important...


I whish Python had piping so we do not need to do something like that.


(2012)


Just stop.


I think this would work in many languages with operator overloading. Same thing in C#: https://gist.github.com/lostmsu/6922f6e1f5b4f07c88358e92d6e8...

Not sure yet if it is possible to make this work for generic operators.


C# 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 +"


By 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.



Discussions on similar submissions:

  1. Hacking Infix Operators into Python with Decorators (February 20, 2016 — 76 points, 12 comments)

Applications are open for YC Summer 2022

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: