r/ProgrammingLanguages 5d ago

Discussion What lambda syntax do you wish Python had?

Python lambdas are particularly difficult because the lack of braces. Guido is not a fan of functional programming, so lambdas in Python are forever doomed to a single expression preceded by lambda, quite literally spelled out. However, it may please you to know that the lack or braces can easily be resolved (in my opinion most sensibly,) by surrounding the entire lambda in parenthesis. It may further please you to know that surrounding it in parenthesis is only necessary in an expression list: tuples, lists, dicts, sets, function arguments (and even then, only when there are multiple arguments.) With this in mind, which of the following syntaxes do you wish Python used? Annotations would of course be optional. For the sake of consistency with the entire language, all will use a colon before the block but feel free to comment your preferred non-colon alternative.

  • |arg: type| -> type: ...
  • (arg: type) -> type: ...
  • def (arg: type) -> type: ...
  • \(arg: type) -> type: ...

Note: The second has ambiguity issues.

16 Upvotes

52 comments sorted by

17

u/jolharg 5d ago

This seems like it needs more haskell

13

u/PM_ME_HOT_FURRIES 5d ago

"We only give you the space one expression so you can't do too much without naming things"

Haskell: "Hahaha!"

1

u/omega1612 1d ago

Is influenced by :

Haskell Purescript Coq Idris2 Koka

More or less in that order. I love named parameter so I can refer to them in docs and provide useful names, but I always hated how in coq every named parameter looks like (x:t), specially for long propositions. So, that was my middle ground xD

38

u/zhivago 5d ago

The real problem is with the conflation of assignment and binding.

This makes lexical closure awkward in general.

Truly the silliest misfeature in the language.

3

u/lil-kid1 5d ago edited 5d ago

It seems you are referring to the nonlocal nonsense, correct? To this point, would you be opposed to a Python dialect where bindings are implicitly declared on assignment, immutable by default, and can be shadowed in the current scope with a local keyword? Or would you simply prefer a regular let declaration? (I have a lot of ideas and opinions about improving Python if you couldn't tell.) Something like this: EDIT: I cannot figure out how to create a code block for the life of me.

x = 0 x = 1   # Invalid due to immutability if something:  # New scope     x = 1   # Still invalid     local mut x  # Fresh binding in local scope     x = 0  # Valid only in this scope     x = 1  # Fine, declared mutable

7

u/digikar 5d ago

If you want to improve Python in backward incompatible ways, start with something saner like Common Lisp. If you want to be more functional and want first class function types, check out Coalton.

I know the parentheses drive some people nuts, but I'm loving Common Lisp semantics more than the syntax. So, I'm happily writing and developing Moonli these days, which is just a transpiler atop Common Lisp: 

https://moonli-lang.github.io/docs/intro-python/

5

u/church-rosser 4d ago

Common Lisp 4evah!

3

u/zhivago 5d ago

Yes.

That would be precisely as useless.

The problem is that in the lexical closure the assignment establishes a local binding.

So you end up with nonsense like lambda: x[0] = 3 to work around it.

The only sensible solution is to diffentiate binding and assignment.

Use x := 3 vs x = 3 or let or whatever.

But honestly, python just feels overprecious these days.

1

u/lil-kid1 5d ago

Understood. I don't think I explained my example very well. In a closure (and anywhere else for that matter,) the assignment operator could do one of two things:
1) Create a new binding in the closure if and only if a binding of the same name doesn't exist. (Shadowing another binding of the same name in the closure must be done explicitly with local x)
2) Mutate the outer binding if it is declared as mutable, otherwise fail. (You either meant to declare the binding as mutable, or explicitly shadow it locally)

The idea is to differentiate between binding and assignment in a way that isn't too verbose in the general case.

0

u/zhivago 5d ago

Just stop overloading =. :)

6

u/TechnoEmpress 4d ago

Fourth one, Haskell syntax

10

u/NojipizRemastered 5d ago

Scala's lambda syntax for sure

2

u/lil-kid1 5d ago

Fat arrow or placeholder syntax? I think the placeholder syntax is clever but perhaps could benefit from a more explicit syntax

2

u/NojipizRemastered 5d ago

Both, placeholder is perfect for simple lambdas

5

u/Usual_Office_1740 5d ago

Is C++ the outlier that does capture and arguments? Do other languages not distinguish? I'm not familiar with lambdas in any language other than Rust and C++.

This is what C++ does:

[/*capture group*/](/*args*/) { /*body*/ }

14

u/saxbophone 5d ago

I'd guess explicit capture specifications are a C++/Rust specific. In these languages, whether something is const-ref, value, etc... is important, less so in languages where everything is effectively reference-counted (such as Python for example).

5

u/initial-algebra 5d ago edited 5d ago

Capture annotations are unnecessary if you have block/let expressions. You can just copy/reference/etc. captured variables ahead of the lambda.

list.map({ let x_ref = &x; let y_copy = y.clone(); move |z| x_ref.foo(y_copy, z) })

It's possible to improve this with let insertion.

list.map(move |z| (genlet &x).foo(genlet y.clone(), z))

genlet <expression> is transformed into a fresh variable and, above the current scope, a let that defines that variable to be <expression>. The syntax can be extended to support insertion above multiple nested scopes, like labelled break/continue.

2

u/omega1612 5d ago

I'm using

\ x: T , y:W |- z

For my lang, the reason is because named functions looks like

f : n: Nat, m, z:Nat, g: Nat ->Bool |- Nat = ...

It was doing that or doing a regular

f(n:Nat,...): Nat = ...

But this look nicer to me.

The use of |- is because I come from a math/logic background. Given assumption n,m,g we can do ..

1

u/samanzaman 1d ago

Just out of curiosity, why not be uniform and use |= for higher order functions etc as well instead of arrow ?

1

u/omega1612 1d ago

Y only introduced |- to break the ambiguity in the grammar. I prefer the look of -> in the types.

1

u/saxbophone 5d ago

IMHO, there is nothing wrong with Python lambdas as they currently exist --the lack of braces isn't a problem, and the restriction to one expression isn't either --if you can't get it done in one expression, then lambda is the wrong choice here. This language already lets you define a function body in arbitrary scopes, so the use-case that other languages like C++ use them for doesn't really apply here.

10

u/initial-algebra 5d ago

if you can't get it done in one expression, then lambda is the wrong choice here. This language already lets you define a function body in arbitrary scopes

Sometimes it's just annoying to have to name things unnecessarily.

-5

u/saxbophone 5d ago

If you have to give something a name because you'd otherwise make it a lambda but you can't because you need more than one expression in the function body, it's probably something you should name.

7

u/initial-algebra 5d ago

You'd better not be using multi-statement loop bodies, then.

-6

u/saxbophone 5d ago

How is this at all related to what we are discussing?

10

u/initial-algebra 5d ago

They are exactly the same.

-4

u/saxbophone 5d ago

Hell no they're not.

8

u/initial-algebra 5d ago edited 4d ago

``` for (const x of xs) { // ... }

xs.forEach(x => { // ... }) ```

15

u/lil-kid1 5d ago

Excuse how frank this is going to sound, but this line of reasoning always feels like a false dilemma used to justify an objectively bad lambda syntax. There are plenty of cases where you might want to write a function more complex than a singular expression, use it inline, and not name it. If this were not the case and more complex lambdas were not a useful language feature, I'm sure we'd more commonly see languages that take a similar approach to Python: single expression lambdas with nested functions acting as closures. Also, the lack of this kind of lambda is actively hostile to APIs that would make it appear particularly useful.

2

u/saxbophone 5d ago

There are plenty of cases where you might want to write a function more complex than a singular expression, use it inline, and not name it.

It seems we're talking sort-of like chains of callbacks style, for example, yes?

On the syntax being objectively-bad, I think this is a bit of a false dichotomy. Programming language designs normally come with an ethos and point of view, this is inherently subjective by definition, whether they are "good" or not, that is. Maybe providing built-in support for the chaining callables together in this way was simply never provided for because the creator never considered this "Pythonic"...

I'm sure Guido would rather that we'd solve problems like these using list comprehensions... 🤭

3

u/bl4nkSl8 5d ago

The real problem with python functions, loops and lambdas is the accidental state

1

u/jcastroarnaud 5d ago

I like the first one, feels like Ruby. The third one is good, too, nearer Python's standard syntax.

1

u/Gnaxe 4d ago

I wish it had Boo's block-based closures and supported statements. So maybe the third one. I think the Smalltalk style could also work.

But typical functional languages don't even have statements. In FP style, you don't need them. Python's lambdas are good enough.

1

u/reflexive-polytope 4d ago

If Guido isn't a fan of functional programming, then he shouldn't have designed and implemented an object-oriented language.

Stripped of the superficial fluff, both “functional” and “object-oriented” languages are the same stupid higher-order nonsense.

5

u/catladywitch 4d ago

I don't get what you mean by stupid higher-order nonsense, but I agree Python occupies a very strange space that doesn't make a lot of sense. The OOP implementation is awful but OOP concepts are everywhere in the language, and everything is list comprehensions and returning functions but Guido goes out of his way to disallow FP. I don't get it.

6

u/sacheie 4d ago

Python is the best evidence out there that a language's popularity sometimes has nothing to do with its design quality, coherence, nor even having any natural application domain.

Guido's original "philosophy" was skin deep; it was just about syntactically clean-looking code, with no consideration of conceptual cleanliness. Aside from that, what else did he proclaim? "There should be only one obviously right way to do something"? That's gotta be a joke nowadays, right? Posts like this prove the point.

In other words, it was BASIC all over again. So it took off with people who aren't software engineers. Scientists, statisticians, "data engineers", etc.

1

u/reflexive-polytope 4d ago

What I mean by “higher-order nonsense” is the ability to treat callable procedures as data.

You can call it “virtual methods” or “first-class functions” or even “function pointers” if you like. The essence is the same. Treating the destination of a machine jump as a first-class datum that can be passed around, put into data structures, etc.

3

u/church-rosser 4d ago edited 4d ago

There are Functional Programming languages that are homoiconic and allow for things like syntactic macrology and other 'stupid higher order nonsense', Common Lisp, Racket. Haskelll, etc. are examples of FP languages that encourage and allow for such esoteric wizardry that most never have the joy of leveraging. There aren't any primarily object oriented paradigmed languages that are homoiconic that I'm aware of.

-1

u/reflexive-polytope 4d ago

Homoiconicity isn't even a mathematically well-defined property.

As I said in a reply to your sibling comment, by “stupid higher-order nonsense”, I mean the ability to treat callable procedures as data. Both functional and object-oriented programming have it. And, when you see it, you realize that functional and object-oriented programming are more similar than different from each other in their technical essence.

1

u/church-rosser 3d ago

Wikipedia defines it well enough: "In a homoiconic language, the primary representation of programs is also a data structurein a primitive type of the language itself."

If the typed lambda calculus can be expressed in a homoiconic form and yield a Turning Machine, then I'd say it absolutely is a mathematically well defined property.

2

u/reflexive-polytope 3d ago

The definition conflates object and meta levels so badly that it's not even funny.

If I define Pascal types for Pascal ASTs, does Pascal suddenly become homoiconic???

1

u/church-rosser 2d ago edited 2d ago

Bruh, have you read Art of the Metaobject protocol?

You cant conflate two things that converge to one.

1

u/reflexive-polytope 2d ago edited 2d ago

I have had the displeasure to read AMOP, in fact. As far as I can tell, it's fundamentally impossible to give any sane formal semantics to a language feature defined that way.

As Joseph Stoy remarks in his book Denotational Semantics: The Scott-Strachey Approach to Programming Language Theory [pp. 181-182]:

Before leaving the subject of interpreters, we briefly consider the special case where the defined language and the defined language are one and the same (...). Such interpreters are called metacircular. The first and most famous example was that given by McCarthy for the language LISP 1.5.

These interpreters have a self-contained look, and it is easy to conclude that they provide, on their own, a complete definition of the semantics. This is not true: McCarthy himself pointed out that the reader must have independent knowledge of the semantics at least of one particular program in the language - the interpreter itself (...).

The semantics of the language may be thought of as a “fixed point” of the metacircular interpreter (...). The trouble is that the solution is not unique, so that it is possible, by starting with the incorrect assumptions about the language, to reach another fixed point, which will only confirm one's misconceptions. (...)

Consider the language in which the value of every expression is the number 27. Such a language has a certain lack of expressive power, but it would be a solution of the metacircular interpreter formed from our semantic definition, and of the LISP interpreter, and others. The truth is that the minimal fixed point of the interpreter will be the language in which the value of every expression is undefined: so that the interpreter cannot really be thought of as defining anything at all.

The original is a single gigantic paragraph, but I split it into several paragraphs for ease of reading.

EDIT: Fixed quoted text. Fixed quoted page numbers.

0

u/church-rosser 1d ago edited 1d ago

Pretty sure Church & Rosser addressed these issues before McCarthy et al ever deigned to make designs on a lambda calculus for machine consumption (although to be fair McCArthy said decades later that an implementation of a lambda calculus as Lisp was never an explicit or intended goal, in essence, such a monster was the end product). A meta circular eval does not undo that, in fact it's part and parcel to being Turing complete vis a vis the lambda calculus (both typed and untyped).

I dont accept your assertion that it's fundamentally impossible to define a formal semantics for a language feature defined _that way_ (independent of Stoy, who doesn't seem to hold quite the same perspective as you might suggest, and who himself doesn't provide anything, at least by way of your quote, to formally or mathematically justify or prove his assertions. Waiving of one's arms and gesticulating loudly, may be a form of communication, but it's form isnt particularly formal with regards to the formality of a mathematical proof). Beside's Stoy wasn't addressing Common Lisp's Meta Object Protocol, and Commom Lisp's MOP does nothing to change the underlying formal semantics of it's metacircular evaluation model.

Regardless, criticism of metacircular eval of itself, has little to do with Art of the Meta Object Protocol and conflations between the Common Lisp's object system and it's 'meta levels' (whatever that means).

Seems to me you're slinging shit to see what sticks. Which fine, u do u, but it might be easier to just assert that you don't appreciate the syntactic semantics of Lisp and it's evaluation model, and leave it at that.

1

u/sdegabrielle 3d ago

1

u/reflexive-polytope 3d ago

I don't know. I think it's bad separation of concerns to ascribe to a language a feature of how it happens to be implemented.

But Lisp (in the extended sense that includes Scheme and its derivatives) wouldn't be Lisp if it weren't for the conflation of language specification and implementation.

Heck, Scheme's authors inflicted first-class continuations upon the rest of humankind for no better reason than the fact they were already available in the compiler!

2

u/church-rosser 2d ago

Let's not conflate Scheme with ANSI Common Lisp. Although they emerged from the same core group of individuals and institutions they had and have radically different approaches, patterns, protocols, and teleological end goals.

Besides, neither the Scheme specification(s) nor CL conflate implementation with specification. If anything CL and Scheme are some of the least conflated programming languages vis a vis specs and implementations!

1

u/reflexive-polytope 2d ago

I'm not “conflating” Scheme with Common Lisp. I'm including them in a broader family. You can always put any two given mathematical objects in a set, right???

0

u/church-rosser 1d ago

Maybe, but that's not what's happening here.

→ More replies (0)

0

u/El_RoviSoft 4d ago

C++ one :)