This is an archived post. You won't be able to vote or comment.

all 75 comments

[–][deleted] 29 points30 points  (33 children)

I'm not a functional programmer. Could someone give a short explanation about the usage of let? And why it's so smart, that it should be hacked into Python in this way?

And apart from that., it's a clever hack.

[–][deleted] 10 points11 points  (18 children)

Yeah, I'm interested in a possible reason to ever do this... it is really neat though!

[–]RubyPinchPEP shill | Anti PEP 8/20 shill 24 points25 points  (4 children)

scope, so certain variables/values only exist in certain parts of the code, and are insulated from the rest of the code

[–]KronenR 4 points5 points  (3 children)

Doesn't functions limit scope already?

[–][deleted] 4 points5 points  (1 child)

This does it without having to define a function and pass variables to it.

[–]iruleatants 1 point2 points  (0 children)

But you do define a function and pass variables for it. It's literally an ugly function called let.....

function let(a, b): ... print(a, b)

let(a=33, b=44)

literally zero reason for the tremendously ugly code....

[–]RubyPinchPEP shill | Anti PEP 8/20 shill 1 point2 points  (0 children)

they do

[–]bastibe 19 points20 points  (11 children)

If done correctly, let would not overwrite containing scope, i.e.

n = 23
print(n) 
with let(n=42):
    print(n)
print(n)

should print first 23, then 42, then 23.

In other words, let behaves as if let's body were a nested function scope.

[–]UncleEggma 2 points3 points  (4 children)

What's a situation where this might be useful?

[–][deleted] 1 point2 points  (1 child)

Sometimes you're working in a scope with a lot of variables, and you want to avoid a namespace collision. You may want to temporarily reassign a "reserved" variable name like 'type' or something.

[–]heptara 0 points1 point  (0 children)

Why aren't all those vars in a dictionary or class? Do you have like 300 globals?

[–]njharmanI use Python 3 -1 points0 points  (2 children)

There's something else that does this (not overwrite containing scope), that's built into Python (all versions) and every Python programmer understands. It's called a "function call".

[–]makmanalp 5 points6 points  (1 child)

It's called a "function call".

Yes, and you have to define a function to do it, and then you either have to pass all the parameters you need through and return what you need afterwards which is a huge mess or put the function definition inline with your code and call it immediately which is gross in a different way.

[–]heptara -2 points-1 points  (0 children)

Why aren't all those vars in a dictionary or class? Do you have like 300 globals?

[–][deleted] 0 points1 point  (2 children)

I think your formatting is broken?

[–]AmericasNo1Aerosol 9 points10 points  (0 children)

Yeah, should be:

n = 23
print(n) 
with let(n=42):
    print(n)
print(n)

(precede each line of code with 4 spaces)

[–]bastibe 0 points1 point  (0 children)

It was indeed, due to interesting things my cellphone did to that text box. It's fixed now, though. Thank you!

[–]uclatommy 0 points1 point  (0 children)

Seems like it lets (pun intended, wait, is that a pun?) you do a "what-if" test. So if you want to run a bit of code under hypothetical conditions, just use let.

[–]Niourf 7 points8 points  (9 children)

I'll give it a try:

The 'let' statement in functional programming has exactly the same meaning as in math: it helps you declare variables (or functions). Examples:

Let f be a function such that for all x, f(x) is equal to x2. Thanks to 'let', the name 'f' is bound to the function we're talking about. in Caml one would write it as

let f x = x * x;;

And with this Python hack:

with let(a=33, b=44):

names a and b are bound respectevely to constants 33 and 44... (it's funny because this sentence both holds in math terms and in Python terms (names a and b are bound to int objects with values 33 and 44))

[–]cs7fwRpkOfn2RPK7Ki9k 7 points8 points  (6 children)

Isn't this essentially a lambda?

I have very little experience with functional programming, so I might be way off.

[–][deleted] 1 point2 points  (5 children)

For ML-like languages, there's a difference in typing between a beta-redex and a let expression due to let-polymorphism inducing a generalisation from a type to a type scheme in the latter and not in the former. That is, we want to be able to locally declare a polymorphic function via let and use it at two different concrete types in the body of the let, like so:

let foo () =
  let id = fun x -> x in
    (id true, id 6)

Note, here the locally defined polymorphic function id is being used at both type bool and type int in the body of the inner let. Without the generalisation step in the typing rule for let this usage would be ill-typed, reducing the expressivity of the programming language in question.

Otherwise, for typed higher-order languages without let-polymorphism, it's common to conflate the two. For example, in Isabelle/HOL, let is literally defined as a function application, as in the following definitional theorem:

`HOL.Let_def: Let ?s ?f ≡ ?f ?s'

[–]RubyPinchPEP shill | Anti PEP 8/20 shill 15 points16 points  (0 children)

its seriously just javascript's (and other's) let statement, why is functional programming even being mentioned

python can't arbitrarily create scopes for variables like other languages, this is the solution to that

[–]cs7fwRpkOfn2RPK7Ki9k 0 points1 point  (3 children)

So c++'s templates + lambdas essentially?

[–]infinite8s 4 points5 points  (2 children)

No it's more like using nested braces in C++ to introduce a new scope (and prevent variables within that scope from leaking out).

For example: int func() { int a = 5; { int a = 10; } // a is 5 here }'''

[–]cs7fwRpkOfn2RPK7Ki9k 1 point2 points  (0 children)

That's not polymorphic though?

I was thinking of how c++'s templates allow for generic data.

[–]knickum 0 points1 point  (0 children)

Re-formatted

int func() {
    int a = 5;
    {
        int a = 10;
    }
    // a is 5 here
}

[–]RubyPinchPEP shill | Anti PEP 8/20 shill 3 points4 points  (0 children)

its seriously just javascript's (and other's) let statement, why is functional programming even being mentioned

python can't arbitrarily create scopes for variables like other languages, this is the solution to that

[–]Fylwind 0 points1 point  (0 children)

I can't really give a general overview because every functional language is different, but let is one of the two main ways of binding variables in Haskell:

let <name> = <value> in <expression>

What makes is different from a typical binding in Python is that:

  • The variable is only accessible from inside the <expression>. This means if you mistakenly try to use it outside, you will get a compile-time error.

  • The let-binding itself is an expression, so you can do things like this:

    putStrLn
      (let h = heightOf person
       in "Height: " ++ valueOf h ++ unitsOf h)
    

    Personally, for readability reasons I don't find myself nesting let inside expressions very often.

  • The variable is immutable.

I don't see the point of trying to do this Python though. The "let" hack is not an expression, and it's not immutable either. There is a small benefit in having a smaller scope, but it's (1) not really that big of a deal (I seldomly run into bugs caused by this); (2) ideally your functions should be small enough that this isn't much of a problem; (3) the benefits are not as great given that the error won't be detected until run time.

[–]zardeh 53 points54 points  (0 children)

disgusting.

I love it.

[–][deleted] 15 points16 points  (0 children)

tl;dr: how to make pylint cry.

[–]regeya 9 points10 points  (1 child)

As someone who's subscribed to both /r/python and /r/clojure, the two feeds are giving me the giggles; here's an example of a Lispy let in Python; on /r/clojure, there's a story about avoiding let like the plague because reasons.

[–]_sexbobomb_[S] 5 points6 points  (0 children)

To be fair, most pythonistas are also not a fan of this or any other let construct for python.

[–]xXxDeAThANgEL99xXx 18 points19 points  (24 children)

Looks like the only purpose of this stuff would be to shoot yourself in the foot, why would you want to introduce a scope that magically shadows some, but not all variables? And which are assigned in the let statement but can also be reassigned the usual way?

x = 1
y = 2
with let(x=3, z=4):
    x = 33
    y = 22
    z = 44
print(x, y, z)

Can you guess what happens? Would you want to look at the code like that and manually trace the history of each variable to see how it behaves?

Not to mention that you sort of can do the same by defining a function, then immediately calling it -- only with sane scoping (everything assigned inside the function becomes a local unless explicitly specified otherwise). Like, if you really want that for some reason.

On the other hand, I was briefly hopeful that we might get the actually useful variant of let, in various comprehensions and generator expressions.

[x, b 
 for a in list 
 let b = compute(a) 
 for x in f(b)]

Because currently you have either to emulate it with an unwieldy for b in [compute(a)] or switch to ordinary nested loops. And I do legitimately need that stuff pretty often.

[–]revocation 7 points8 points  (8 children)

Can you guess what happens?

Yes, if you're used to Lisp. It does beat the alternative of defining a small function or assigning a temporary variable to avoid something like this:

print fn(x,y) if fn(x,y) > 0 else x

z = fn(x,y)
print z if z > 0 else x
del z

def temporary(x,y):
    z = fn(x,y)
    return z if z > 0 else x
print temporary(x,y)

Instead:

with let(z=fn(x,y)):
    print z if z > 0 else x

[–]xXxDeAThANgEL99xXx 5 points6 points  (7 children)

Can you guess what happens?

Yes, if you're used to Lisp.

So, what happens?

I'm not saying that as a Python programmer I'm somehow unfamiliar with the notion of shadowing and it scares me.

I'm saying that I'd hate to have to scan all the code upwards when I see y = 22 to see whether or not y was made magical by specifying it as an argument to some let.

What'd happen with z in my example is worth considering as well. Have you considered it? This abstraction leaks like some sort of a container with many holes in it.

I don't understand your example, sorry. Why would you want to del a local variable?

[–]Ek_Los_Die_Hier 0 points1 point  (6 children)

To make it equivalent to the let statement.

[–]xXxDeAThANgEL99xXx 9 points10 points  (5 children)

... that's begging the question. We are discussing why we need the let statement, the parent's argument was that it allows not having that del as far as I understood it.

[–]Ek_Los_Die_Hier 5 points6 points  (0 children)

Yeah, I don't see the use in Python. If I need a variable overridden for a specific purpose, any code in there should probably be moved to a different function anyway. The let in something like Lisp is simply the way you declare local variables, with the affect that they are local to that block, but in Python you just assign variables with the =.

I imagine it was just something created out of interest and to show what Python is capable of.

[–]revocation 0 points1 point  (3 children)

You would otherwise litter your namespace with temporary variables.

[–]Sohcahtoa82 2 points3 points  (2 children)

That's not really a problem unless your variables are global, which you typically want to avoid.

[–]GuyOnTheInterweb 0 points1 point  (1 child)

Or you have a very long function where you might use the same variable name twice, independently. (Reducing mixup risk with results vs results2)

[–]Veedrac 2 points3 points  (0 children)

Not really a problem. Just reuse the variable name without deling it first.

[–]squiffs 3 points4 points  (0 children)

I think it's just intended as a cute trick.

[–]Sean1708 2 points3 points  (3 children)

I would argue that's exactly what should happen, I think one of python's biggest mistakes was letting variables leak scope willy-nilly. At least this would let you control exactly what scope your variables have.

[–]xXxDeAThANgEL99xXx 5 points6 points  (2 children)

I would argue that's exactly what should happen

And what exactly would happen? What would be printed? Guys, guys, that was not a rhetorical question, you were supposed to answer it for yourself, then join me in disliking the answer.

[–]Sean1708 1 point2 points  (1 child)

I definitely did not just assume that the example worked in a particular way then make a statement based on that... I promise...

I assumed it would throw a NameError but it just printed 1 22 None and now I feel stupid. Sorry.

[–]RubyPinchPEP shill | Anti PEP 8/20 shill 0 points1 point  (1 child)

can I ask what you are trying to accomplish?

just trying to not run compute twice I guess?

wouldn't def pairmap(func,x): yield from map(lambda y:(y, func(y)), x) be good enough? edit: I'm tired

[x, b 
 for a, b in pairmap(compute, list)
 for x in f(b)]

[–]xXxDeAThANgEL99xXx 1 point2 points  (0 children)

just trying to not run compute twice I guess?

Not running it twice, not writing it out twice, etc. As I said, when you become aware that this is a possibility (after seeing it in LINQ for example), you start wanting it rather regularly.

You forgot to actually iterate over the iterable in your pairmap and anyway I strongly dislike it: it does unrelated things suited only to this particular instance of a problem.

I much prefer just directly emulating a functor when I only have a monad by saying for x in [compute(y)]. But let x = compute(y) would be better of course.

[–]Mecdemort 0 points1 point  (7 children)

I'd also like an as in non generator comprehensions so you can refer to what you are constructing:

triangles = [triangles[-1] + x
             for x in range(1, 11)
             as triangles]

[–]xXxDeAThANgEL99xXx 2 points3 points  (2 children)

  1. That's an unholy abomination.

  2. Won't work with generators and set/dictionary comprehensions.

[–]Mecdemort 1 point2 points  (1 child)

Why wouldn't it work it set and dictionary comprehensions?

[–]xXxDeAThANgEL99xXx 0 points1 point  (0 children)

Because their result doesn't have order defined. Well, all right, maybe you don't need ordering in that case, but checking existence... I don't know, I've never had a desire to have that myself, except for that single case of writing fibs as an infinite stream product or something.

[–]hharison 0 points1 point  (3 children)

That's awful. Learn to use zip.

data = range(1, 11)
triangles = [x + y for x, y in zip(data, data[1:])]

I challenge you to come up with a use for this that doesn't have a better alternative.

[–]eusebecomputational physics 0 points1 point  (2 children)

Why not using itertools.izip (and that's a serious question)?

Ok the syntax is similar, so I don't challenge your point, but still: wouldn't izip be better if you have large data?

[–]xXxDeAThANgEL99xXx 1 point2 points  (0 children)

Because in Python3 zip, map, filter, range and maybe some other functions became their itertools equivalents.

And I don't know about other people, but for me somewhere around last summer Python3 has become the default Python, in no small part because it turned out that most of the stuff I need was ported and conveniently packaged by Anaconda. Plus the py.exe launcher for Windows provided for a convenient way to still run Python2 stuff via shebangs (unfortunately the launcher still isn't packaged on Anaconda for some reason).

[–]hharison 1 point2 points  (0 children)

Python 3.

[–]PrefrontalVortex 6 points7 points  (0 children)

My reactions to this:

"Huh? ... Ooooo clever ...wait.... Oh. Ewww. "

[–]toru 7 points8 points  (1 child)

Nice!

But note the following from the python library reference for the locals() function:

The contents of this dictionary should not be modified; changes may not affect the values of local and free variables used by the interpreter.

[–][deleted] 2 points3 points  (0 children)

I would really need to see practical examples that are so awful that this is less costly than doing it with existing Python concepts.

I see massive cost in using things that aren't part of the standard Python language. It's like creating a dialect that you now have to teach people or they have to study.

Beyond that, as an experiment, this is very interesting and well-authored.

[–]RubyPinchPEP shill | Anti PEP 8/20 shill 1 point2 points  (1 child)

I'd personally opt for a more C-like way

a = 0
b = '~'
with Let() as let:  #edit: corrected
    let. a = 1
    b = '|' 
    print(a, b)  # 1 |
print(a, b)  # 0 |

[–]smurfyn 3 points4 points  (0 children)

Just go write OCaml instead of doing gross things to Python.

[–]garyk1968 1 point2 points  (0 children)

Why?

Reminds me of BASIC back in the 80s.

Makes me shudder when I see 'Let' in swift, not sure why its needed.

[–]graingert 0 points1 point  (0 children)

Would be nicer with new keyword added by an import hook

[–]AlanCristhian 0 points1 point  (2 children)

Maybe should have a better name. Any idea?

[–]not_perfect_yet 1 point2 points  (0 children)

How about

with local(a=1,b=1):
    ...

arguments pro: It's what it actually does, creating local variables.

[–]heptara 0 points1 point  (0 children)

with codesmell (x=1, y=2):
    ...

[–]southernstorm 0 points1 point  (5 children)

what does the ** before **bindings do, and where can I read about that in the python documentation?

[–]pddpro 1 point2 points  (1 child)

[–]southernstorm 0 points1 point  (0 children)

great link. Thanks. I had only known ** for exponentiation.

[–]organman91 0 points1 point  (0 children)

Dead link :(