Is there a Python equivalent of the Haskell 'let'

Question:

Is there a Python equivalent of the Haskell ‘let’ expression that would allow me to write something like:

list2 = [let (name,size)=lookup(productId) in (barcode(productId),metric(size)) 
            for productId in list]

If not, what would be the most readable alternative?

Added for clarification of the let syntax:

x = let (name,size)=lookup(productId) in (barcode(productId),metric(size))

is equivalent to

(name,size) = lookup(productId)
x = (barcode(productId),metric(size))

The second version doesn’t work that well with list comprehensions, though.

Asked By: Perseids

||

Answers:

There is no such thing. You could emulate it the same way let is desugared to lambda calculus (let x = foo in bar <=> (x -> bar) (foo)).

The most readable alternative depends on the circumstances. For your specific example, I’d choose something like [barcode(productId), metric(size) for productId, (_, size) in zip(productIds, map(lookup, productIds))] (really ugly on second thought, it’s easier if you don’t need productId too, then you could use map) or an explicit for loop (in a generator):

def barcodes_and_metrics(productIds):
    for productId in productIds:
        _, size = lookup(productId)
        yield barcode(productId), metric(size)
Answered By: user395760

You could use a temporary list comprehension

[(barcode(productId), metric(size)) for name, size in [lookup(productId)]][0]

or, equivalently, a generator expression

next((barcode(productId), metric(size)) for name, size in [lookup(productId)])

but both of those are pretty horrible.

Another (horrible) method is via a temporary lambda, which you call immediately

(lambda (name, size): (barcode(productId), metric(size)))(lookup(productId))

I think the recommended “Pythonic” way would just be to define a function, like

def barcode_metric(productId):
   name, size = lookup(productId)
   return barcode(productId), metric(size)
list2 = [barcode_metric(productId) for productId in list]
Answered By: huon

Only guessing at what Haskell does, here’s the alternative. It uses what’s known in Python as “list comprehension”.

[barcode(productId), metric(size)
    for (productId, (name, size)) in [
        (productId, lookup(productId)) for productId in list_]
]

You could include the use of lambda:, as others have suggested.

Answered By: vsh

To get something vaguely comparable, you’ll either need to do two comprehensions or maps, or define a new function. One approach that hasn’t been suggested yet is to break it up into two lines like so. I believe this is somewhat readable; though probably defining your own function is the right way to go:

pids_names_sizes = (pid, lookup(pid) for pid in list1)
list2 = [(barcode(pid), metric(size)) for pid, (name, size) in pids_names_sizes]
Answered By: senderle

Recent python versions allows multiple for clauses in a generator expression, so you can now do something like:

list2 = [ barcode(productID), metric(size)
          for productID in list
          for (name,size) in (lookup(productID),) ]

which is similar to what Haskell provides too:

list2 = [ (barcode productID, metric size)
        | productID <- list
        , let (name,size) = lookup productID ]

and denotationally equivalent to

list2 = [ (barcode productID, metric size) 
        | productID <- list
        , (name,size) <- [lookup productID] ]
Answered By: b0fh

The multiple for clauses in b0fh’s answer is the style I have personally been using for a while now, as I believe it provides more clarity and doesn’t clutter the namespace with temporary functions. However, if speed is an issue, it is important to remember that temporarily constructing a one element list takes notably longer than constructing a one-tuple.

Comparing the speed of the various solutions in this thread, I found that the ugly lambda hack is slowest, followed by the nested generators and then the solution by b0fh. However, these were all surpassed by the one-tuple winner:

list2 = [ barcode(productID), metric(size)
          for productID in list
          for (_, size) in (lookup(productID),) ]

This may not be so relevant to the OP’s question, but there are other cases where clarity can be greatly enhanced and speed gained in cases where one might wish to use a list comprehension, by using one-tuples instead of lists for dummy iterators.

Answered By: dalum

Although you can simply write this as:

list2 = [(barcode(pid), metric(lookup(pid)[1]))
         for pid in list]

You could define LET yourself to get:

list2 = [LET(('size', lookup(pid)[1]),
             lambda o: (barcode(pid), metric(o.size)))
         for pid in list]

or even:

list2 = map(lambda pid: LET(('name_size', lookup(pid),
                             'size', lambda o: o.name_size[1]),
                            lambda o: (barcode(pid), metric(o.size))),
            list)

as follows:

import types

def _obj():
  return lambda: None

def LET(bindings, body, env=None):
  '''Introduce local bindings.
  ex: LET(('a', 1,
           'b', 2),
          lambda o: [o.a, o.b])
  gives: [1, 2]

  Bindings down the chain can depend on
  the ones above them through a lambda.
  ex: LET(('a', 1,
           'b', lambda o: o.a + 1),
          lambda o: o.b)
  gives: 2
  '''
  if len(bindings) == 0:
    return body(env)

  env = env or _obj()
  k, v = bindings[:2]
  if isinstance(v, types.FunctionType):
    v = v(env)

  setattr(env, k, v)
  return LET(bindings[2:], body, env)
Answered By: divs1210

Since you asked for best readability you could consider the lambda-option but with a small twist: initialise the arguments. Here are various options I use myself, starting with the first I tried and ending with the one I use most now.

Suppose we have a function (not shown) which gets data_structure as argument, and you need to get x from it repeatedly.

First try (as per 2012 answer from huon):

(lambda x:
    x * x + 42 * x)
  (data_structure['a']['b'])

With multiple symbols this becomes less readable, so next I tried:

(lambda x, y:
    x * x + 42 * x + y)
  (x = data_structure['a']['b'],
   y = 16)

That is still not very readable as it repeats the symbolic names. So then I tried:

(lambda x = data_structure['a']['b'],
        y = 16:
  x * x + 42 * x + y)()

This almost reads as an ‘let’ expression. The positioning and formatting of the assignments is yours of course.

This idiom is easily recognised by the starting ‘(‘ and the ending ‘()’.

In functional expressions (also in Python), many parenthesis tend to pile up at the end. The odd one out ‘(‘ is easily spotted.

Answered By: Erik
class let:
    def __init__(self, var):
        self.x = var

    def __enter__(self):
        return self.x

    def __exit__(self, type, value, traceback):
        pass

with let(os.path) as p:
    print(p)

But this is effectively the same as p = os.path as p‘s scope is not confined to the with block. To achieve that, you’d need

class let:
    def __init__(self, var):
        self.value = var
    def __enter__(self):
        return self
    def __exit__(self, type, value, traceback):
        del var.value
        var.value = None

with let(os.path) as var:
    print(var.value)  # same as print(os.path)
print(var.value)  # same as print(None)

Here var.value will be None outside of the with block, but os.path within it.

Answered By: crizCraig

In Python 3.8, assignment expressions using the := operator were added: PEP 572.

This can be used somewhat like let in Haskell, although iterable unpacking is not supported.

list2 = [
    (lookup_result := lookup(productId), # store tuple since iterable unpacking isn't supported
     name := lookup_result[0], # manually unpack tuple
     size := lookup_result[1],
     (barcode(productId), metric(size)))[-1] # put result as the last item in the tuple, then extract on the result using the (...)[-1]
    for productId in list1
]

Note that this is scoped like a normal Python assignment, e.g. if used inside a function, the variables bound will be accessible throughout the entire function, not just in the expression.

Answered By: Oli