Uniform Function Call Syntax (UFCS)

Discussion in 'Python' started by jongiddy, Jun 7, 2014.

  1. jongiddy

    Ian Kelly Guest

    But that would all be done in getattr, so I don't think it affects
    hasattr's implementation at all. Since hasattr doesn't push anything
    onto the stack, getattr doesn't have to care whether it was called
    directly from Python or indirectly via getattr; either way the scope
    it needs is just the top frame of the stack.

    Could be a different matter in other implementations, though.
    Ian Kelly, Jun 8, 2014
    1. Advertisements

  2. jongiddy

    jongiddy Guest

    In CPython, the UFCS would not be done in PyObject_GetAttr() as that would affect hasattr() as well. Instead, it would be implemented in the bytecode for LOAD_ATTR. If LOAD_ATTR was about to return an AttributeError, e.g. for[].len, it would perform the equivalent of a LOAD_NAME operation, with thedifference that if the name is not found or is not callable, it returns AttributeError instead of NameError.

    If the name is found, then it would return something: for [].len, it would return the len() function wrapped to know that it's first argument was the list, which might be done by creating a fake Method object, as shown in Ian's code.

    But getattr([], 'len') and hasattr([], 'len') would both return False.

    I'm beginning to think it is too un-Pythonic - too much implicitness, unless it can be spelt differently, something like [].len(_) or [].len(...) to explicitly indicate that it plans to call a function, but might call a method if one is available.
    jongiddy, Jun 8, 2014
    1. Advertisements

  3. ]
    As far as I am concerned, the biggest problem with chained method calls
    is that it encourages long one-liners. But I think chained calls are
    quite natural to read, and rather similar to the postfix notation used by

    "foo.jpg" read_file JPEG_decode autocrop 0.5 rotate grey PNG_encode
    "foo.png" write_file

    Although Forth has a (justified) reputation for being hard to read,
    postfix notation is not the cause. The above can be understood easily as
    a chain of function calls: read the file, then decode, then autocrop,
    then rotate, they grey, then encode, then write the file. You read and
    write the calls in the same first-to-last order as you would perform them.

    The equivalent prefix notation used by function calls is unnaturally
    backwards and painful to read:

    read_file("foo.jpg"))), 0.5))), "foo.png");

    I'm not sure how this is *syntactic* ambiguity.

    As I see it, the only syntactic ambiguity occurs when you have functions
    of two arguments. Using shell notation:

    plus(1, 2) | divide(2)

    Assuming divide() takes two arguments, does that give 3/2 or 2/3? I would
    expect that the argument being piped in is assigned to the first
    argument. But I'm not sure how this sort of design ambiguity is fixed by
    importing names into the current namespace.

    (Note that Forth is brilliant here, as it exposes the argument stack and
    gives you a rich set of stack manipulation commands.)

    While we're talking about chaining method and function calls, I'll take
    the opportunity to link to this, in case anyone feels like adapting it to

    Steven D'Aprano, Jun 9, 2014
  4. The ambiguity I'm talking about here is with the dot. The original
    version has "Stdio.read_file" as the first function called; for a
    Python equivalent, imagine a string processing pipeline and having
    "re.sub" in the middle of it. You can't take "re.sub" as the name of
    an attribute on a string without some fiddling around that completely
    destroys the point of data-flow syntax. So I cheated, and turned
    everything into local (imported) names (adorning the ones that needed
    it). This is a bad idea in Pike for the same reason it's a bad idea in
    Python - you end up with a massively polluted global namespace.

    This could be solved, though, by having a completely different symbol
    that means "the thing on my left is actually the first positional
    parameter in the function call on my right", such as in your example:
    This would be absolutely identical to:

    divide(plus(1, 2), 2)

    Maybe you could even make it so that:

    plus(1, 2) x=| divide(y=2)

    is equivalent to

    divide(x=plus(1, 2), y=2)

    for the sake of consistency, and to allow the pipeline to inject
    something someplace other than the first argument.

    I'm not sure whether it'd be as useful in practice, though. It would
    depend partly on the exact syntax used. Obviously the pipe itself
    can't be used as it already means bitwise or, and this needs to be
    really REALLY clear about what's going on. But a data-flow notation
    would be of value in theory, at least.

    Chris Angelico, Jun 9, 2014
  5. jongiddy

    Roy Smith Guest

    As does PostScript (which, despite its reputation as a printer format,
    is really a full-fledged programming language). I suspect that people
    who didn't grow up with RPN (i.e. H/P calculators) find it amazingly
    obtuse. In much the same way I find Objective-C amazingly obtuse. Oh,
    wait, that's the other thread.
    Roy Smith, Jun 9, 2014
  6. Given UFCS, that ought to return the global squared function, curried
    with 3 as its first (and only) argument.

    UFCS would be a pretty big design change to Python, but I don't think it
    would be a *problem* as such. It just means that x.y, hasattr(x, y) etc.
    would mean something different to what they currently mean.

    Yes. And this is a problem why?

    Obviously it would mean that the semantics of hasattr will be different
    than they are now, but it's still a coherent set of semantics.

    In fact, one can already give a class a __getattr__ method which provides
    UFCS functionality. (Hmmm, you need a way to get the caller's globals.
    You know, this keeps coming up. I think it's high-time Python offered
    this as a supported function.) That's no more a problem than any other
    dynamically generated attribute.

    Stick that __getattr__ in object itself, and UFCS is now language wide.
    That would make an awesome hack for anyone wanting to experiment with

    hasattr doesn't care about globals, nor does it need to. hasattr behaves
    like the equivalent to:

    def hasattr(obj, name):
    except AttributeError:
    return False
    return True

    give or take. And yes, if accessing your attribute has side effects,
    using hasattr does too:

    py> class Spam(object):
    .... @property
    .... def spam(self):
    .... print("Spam spam spam spam LOVERLY SPAAAAM!!!!")
    .... return "spam"
    py> x = Spam()
    py> hasattr(x, "spam")
    Spam spam spam spam LOVERLY SPAAAAM!!!!

    If that's a worry to you, you can try inspect.getattr_static.


    class Example:
    def __getattr__(self, name):
    if name == 'module_name':
    if __name__ == '__main__':
    return "NOBODY expects the Spanish Inquisition!"
    return __name__
    raise AttributeError("no attribute %r" % name)

    Steven D'Aprano, Jun 9, 2014

  7. Interface on the left, implementation on the right. That's especially
    obvious when you consider operators like < + - * etc.

    Consider x + y. What happens?

    #1 First, Python checks whether y is an instance of a *subclass* of x. If
    so, y gets priority, otherwise x gets priority.

    #2 If y gets priority, y.__radd__(x) is called, if it exists. If it
    returns something other than NotImplemented, we are done.

    #3 However if y.__radd__ doesn't exist, or it returns NotImplemented,
    then Python continues as if x had priority.

    #3 If x has priority, then x.__add__(y) is called, if it exists. If it
    returns something other than NotImplemented, we are done.

    #4 However if it doesn't exist, or it returns NotImplemented, then
    y.__radd__(x) is called, provided it wasn't already tried in step #2.

    #5 Finally, if neither object has __add__ or __radd__, or both return
    NotImplemented, then Python raises TypeError.

    That's a lot of boilerplate if you were required to implement it yourself
    in every single operator method. Better, Python handles all the boiler
    plate, all you have to do is just handle the cases you care about, and
    return NotImplemented for everything else.
    Steven D'Aprano, Jun 9, 2014
  8. Coherent perhaps, but in direct opposition to the OP's statement about
    how hasattr should return False even if there's a global to be found.

    A coherent meaning for this kind of thing would almost certainly not
    be possible within the OP's requirements, although it's entirely
    possible something sensible could be put together.

    (By the way, would (3).squared return a curried reference to squared
    as of when you looked it up, or would it return something that
    late-binds to whatever 'squared' is in scope as of when you call it?
    If the latter, then hasattr would have to always return True, and
    getattr would have to return something that does the late-bind lookup
    and turns NameError into AttributeError.)

    Chris Angelico, Jun 9, 2014
  9. In a word, Python has predefined a handful of *generic
    functions/methods*, which are general and standard in GOOPS (Guile's
    object system):

    (define-method (+ (x <string>) (y <string)) ...)
    (define-method (+ (x <matrix>) (y <matrix>)) ...)
    (define-method (+ (f <fish>) (b <bicycle>)) ...)
    (define-method (+ (a <foo>) (b <bar>) (c <baz>)) ...)

    <URL: http://www.gnu.org/software/guile/manual/html_node/

    Marko Rauhamaa, Jun 9, 2014
  10. jongiddy

    jongiddy Guest

    Perhaps a pipeline symbol plus an insertion marker would work better in Python:

    plus(1, 2) ~ divide(x=^, y=2)

    f.readlines() ~ map(int, ^) ~ min(^, key=lambda n: n % 10).str() ~ base64.b64encode(^, b'?-') ~ print(^)

    Stdio.read_file("foo.jpg") ~ Image.JPEG_decode(^).autocrop().rotate(0.5).grey() ~ Image.PNG_encode(^) ~ Stdio.write_file("foo.png", ^)
    jongiddy, Jun 9, 2014

  11. That's nine words :)
    Steven D'Aprano, Jun 9, 2014
  12. Chris Angelico, Jun 9, 2014

  13. In what language does "often" (\ˈȯ-fən, ÷ˈȯf-tən\) sound like
    "orphan" (\ˈȯr-fən\)?

    ('oh-fan', 'ov-ten' or even 'off-ten' versus 'or-fen')

    No wonder Gilbert and Sullivan had difficulty seeking success after HMS

    Steven D'Aprano, Jun 9, 2014
  14. jongiddy

    Ian Kelly Guest

    On Jun 8, 2014 9:56 PM, "Steven D'Aprano"
    Earlier in this thread I pointed out that returning True creates problems
    for duck typing. But I'm now convinced that's preferable to making getattr
    and hasattr inconsistent.
    Ian Kelly, Jun 9, 2014
  15. Language? English. :) Your point is more about accent, and if you
    listen to some of the accents around the English countryside, you'll
    know there are quite a few of them (which is a plot point in My Fair
    Lady). Personally, I think the Midlands accents are rather delightful.
    I've spent a month at a time in Buxton (one of the highest-altitude
    towns in England) several times, always a pleasant time. But anyway.
    The story is set in Penzance, in Cornwall, and in the typical Cornish
    accent, the two words are fairly much alike. That said, though, some
    performers (either through sloppiness or for deliberate comic effect)
    do play around with that joke; but it's not hard to establish accents
    that make the joke work.
    Hmm, that's a good idea! I wonder whether we could use it... you know,
    same as so many other people already have. (Starship Pinafore
    productions are actually fairly common) Wonder what's in our
    calendar... ooh look, later on this year!


    Anyone want to come and join us? I'll be up in the lighting box every night..

    Chris Angelico, Jun 9, 2014
  16. jongiddy

    jongiddy Guest

    So, just to summarise the discussion:

    There was some very mild support for readable pipelines, either using UFCS or an alternative syntax, but the Pythonic way to make combinations of function and method applications readable is to assign to variables over multiple lines. Make the code read down, not across.

    The idea that a class method could override a function using UFCS didn't get much traction. From Zen of Python, "explicit is better than implicit" means no differences in behaviour, depending on context. The fact that x.y andx.__getattr__ may behave differently under UFCS is also a problem. Since hasattr testing and AttributeError catching are both commonly used now, thiscould cause real problems, so could probably not be changed until Python 4..

    Finally, Gilbert & Sullivan are definitely due a revival.
    jongiddy, Jun 10, 2014
    1. Advertisements

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments (here). After that, you can post your question and our members will help you out.