Hacker News new | ask | show | jobs
by Goladus 5169 days ago
Interesting, but I'm having a hard time seeing how this syntax saves me much in terms of code size and complexity compared to just wrapping the code blocks into named functions and passing those.
1 comments

The problem with that is that it forces moving the code above where it is passed, which can greatly clutter things.

For example, consider the function "forever" in Haskell:

  forever action = do
    action
    forever action
Now you can use it as a new control structure, e.g:

  forever $ do
    (sock, addr) <- accept listener
    forkIO $ handleClient sock
The $ simply means "apply" and is low-precedence, so it removes the need to put () around the entire argument to be applied.

In Python, you could define:

  def forever(action):
    action()
    forever(action)
but then, to use it, you have to give a name to your function, so:

  def accept_once():
    (sock, addr) = listener.accept()
    fork(partial(handleClient, sock))
  forever(accept_once)
This makes "forever" much less useful as a new control structure/looping primitive.

In this sense, Python makes DSLs less usable. The built-in primitives are first-class, and can have code directly within their use. Library functions are second-class, and can only have code passed by name which must then fully appear before the use.

Another example is callbacks. The reason "twisted" is probably called "twisted", and that people hate callbacks so much, is that it forces writing the code in backwards order, precisely because of this problem. For example:

  def handle_result(result):
    print "Done:", result
  def connection_started(conn):
    conn.request(SomeRequest(), handle_result)
  def start():
    start_connecting(connection_started)
Compare this with Haskell, as an example:

  start = startConnecting $ \conn -> do
    request conn SomeRequest $ \result -> do
      putStrLn $ "Done: " ++ show result
Note, in Haskell, this would actually be worked out to be (by overloading the semicolon):

  start = do
    conn <- startConnecting
    result <- request conn SomeRequest
    putStrLn $ "Done: " ++ show result
But even the former nested representation is better than the backwards ("twisted") representation that makes people hate callbacks so much.
But you don't have to write your callbacks in reverse order in python:

  >>> def fight(batman_wins):
  ...     def t(): return "pow"
  ...     def f(): return "oof"
  ...     def b(msg): print msg
  ...     # if_but not defined at this point
  ...     if_but(lambda: batman_wins, t, f, b)
  ...
  >>> def if_but(cond, true, false, but):
  ...     if cond():
  ...       v = true()
  ...     else:
  ...       v = false()
  ...     but(v)
  ...
  >>> fight(True)
  pow
  >>> fight(False)
  oof
Sure, if you want to call if_but outside a function, it has to be defined, but inside a function (as in your example, and in 99% of python development that doesn't occur in a REPL) x() is effectively globals()['x'](). Have I totally misunderstood your point?
This problem is indeed less severe when the names are defined in a non-local namespace (inside a class, or as globals).

But callback-style often appeals to defining very-local callback code. In fact, it would be anonymous in languages that support it, but it is forced to not only be given a name, but also potentially clutter some namespace (or be written in reverse order).

Consider having every for/while loop, or every "if" require giving a name to the code block[s] within it. It sounds insane. The same doesn't sound insane for library functions, but in my opinion that's really just because everybody's used to this limitation.

When I moved from Python to Haskell, one of the many joys was that I could stick an anonymous code block anywhere so easily.

Thanks for the explanation, that makes sense.
This seems like a solution in search of a problem. There is a nice 'forever' construct. It's called 'while':

    while True:
      action()
I know what you mean about generalising this to other control structures, but it looks like the whole concept starts from "I want to write 'forever' that resembles something from other languages", rather than "I want to write a loop". There are existing ways to write things like that, so is do we really need to force something from other languages into Python? What about some examples which cannot be easily handled - maybe the solution for them is something completely different than porting codeblocks.
You are missing the point. The forever structure is just a code example. What's missing is the ability to write your own control structures generally. (Some of which won't already exist.)
That is exactly what I meant - this is a synthetic example. The article has synthetic examples. People want code blocks, but no one is really showing why. Own control structures are cool, ok, but what are you trying to solve with them? Where are the real, convincing before and after examples that are not easily solvable otherwise?
Here's some Haskell code I really like, and I think it would be far less nice in Python:

  replicateM 10 . forkIO . forever $ do
    ..
Which is basically the same as:

  replicateM 10 (forkIO (forever (do ..)))
replicateM 10 executes its code block argument 10 times. forkIO executes its code block argument in a new thread. forever loops forever.

So this line basically creates a thread pool with 10 threads, all infinitely executing the given code block.

How would you solve this in Python?

> How would you solve this in Python?

The question is how does codeblocks solve this problem in Python, not how does Haskell solve this problem in Haskell.

I can honestly say that the ability to write your own control structures is one of the quickest ways to make me hate developing in a particular language. Any time you use them, your code becomes significantly less readable, and significantly less portable.

It all just becomes a giant clusterfuck way too quickly. Small set of well-defined semantics, please.

Any time you use them, your code becomes significantly less readable, and significantly less portable.

Specific examples?

I've actually often found the opposite to be true, given a good (meta) syntax for handling this.

I've often seen patterns in Smalltalk like:

    myTransaction 
        commit: [
            "Stuff happens here"
        ]
        onRollback: [   
            "handles rollback"
        ].
Semaphores are used for critical sections like so:

    (mySemaphore)
        critical: [
            "critical section code goes here"
        ].
I think these examples show how such a thing can increase readability.

As far as the portability thing goes, I've never heard of that being a terrible problem in Smalltalk. If code from one environment has some custom method like "isNilOrFalse:" or something like "ifNotNilDo:" then it's a simple matter to port that code over another environment, or have it syntactically rewritten by the browser to make a portable library. The sorts of patterns I list above are generally for DSLs that are only used internally in a given library or for code that uses the particular library, so portability isn't a problem. The structures are carried where needed by the library.

It all just becomes a giant clusterfuck way too quickly. Small set of well-defined semantics, please.

Smalltalk does have a small set of well-defined semantics, just not at the level you're used to. I have never heard of modifications that break the control flow standard library. My experience is that good programmers don't change how the "standard" behaviors work, because those changes tend to break the rest of the code base.

I understand anonymous code blocks is something that python doesn't do natively and other languages can do. What I don't understand is why it's worth the bother, given the hoops one must jump through to get it to work in Python specifically (new operators, keywords, and evaluating string literals as code).

If you really need new control structures, Python syntax isn't all that hard to hack.

No string literals are evaluated as code. Everything is rewritten and then evaluated and executed at full speed.
Surely the act of enhancing a function like that, where you don't want the original code to be available, is more commonly dealt with in Python using decorators?