[HN Gopher] On the proliferation of try, and soon, await (2020)
       ___________________________________________________________________
        
       On the proliferation of try, and soon, await (2020)
        
       Author : gpderetta
       Score  : 56 points
       Date   : 2021-08-24 12:21 UTC (2 days ago)
        
 (HTM) web link (forums.swift.org)
 (TXT) w3m dump (forums.swift.org)
        
       | ehttwljlq34y wrote:
       | Mods are downvoting and covering up the fact that Biden is a
       | murderous piece of shit.
       | 
       | Multiple explosions in Kabul _killing_ American citizens.
        
       | lalaithion wrote:
       | The original poster says that these functions can't ever have
       | invariants broken:
       | 
       | > Functions that only mutate instances of well-encapsulated types
       | through their public APIs. That includes nearly all functions
       | that are not methods.
       | 
       | > Methods of well-encapsulated types that only mutate non-member
       | instances of other well-encapsulated types through their public
       | APIs.
       | 
       | This is incorrect! Consider the following function (in
       | psuedocode):                   func insert(id: Int, object:
       | ThingInSpace) {           ids.update(id);           var location
       | = get_location(object);           locations[id] = location;
       | var metadata = compute_metadata(object);           metadatas[id]
       | = metadata;         }
       | 
       | If compute_metadata or get_location throw, then even though we're
       | only modifying dictionaries or sets through their public APIs, we
       | can still invalidate an invariant that affects our program
       | correctness. This function is safe if and only if get_location
       | and compute_metadata cannot throw. If they can throw, this should
       | be rewritten as                   func insert(id: Int, object:
       | ThingInSpace) {           var location = get_location(object);
       | var metadata = compute_metadata(object);
       | ids.update(id);           locations[id] = location;
       | metadatas[id] = metadata;         }
       | 
       | Adding "try" to the original function makes it clear that this
       | function can destroy the invariant of ids, locations, and
       | metadatas, and it needs to be rearranged to be the second
       | function in order for the code to be correct.
        
         | nine_k wrote:
         | BTW thank you for reminding what a pain mutable data are, and
         | doubly so, mutable data that can be put into invalid by merely
         | calling legitimate methods.
        
         | b3morales wrote:
         | Your premise assumes that `insert` will abort immediately if
         | `get_location` or `compute_metadata` throw, but that's an
         | arbitrary (poor) decision by the author of `insert`. The errors
         | can (should) be caught and the relationship between `locations`
         | and `metadatas` restored in the handler.
         | 
         | And there's certainly no reason for `insert` to _re-throw_ its
         | helpers ' internal errors. What would its caller do with them?
        
           | lalaithion wrote:
           | I agree. A function that properly handles its helpers
           | internal errors is better than a function that simply
           | rethrows them. However, both are better than a function that
           | simply rethrows them AND leaves the application in an
           | inconsistent state. Additionally, adding the "try" syntax
           | makes it clear that "insert" has been written poorly. For
           | example, consider the code:                   func insert(id:
           | Int, object: ThingInSpace) {           var location = try
           | get_location(object);           ids.update(id);
           | locations[id] = location;           metadatas[id] =
           | compute_metadata(object);         }
           | 
           | Now I can be sure that compute_metadata doesn't fail, so it's
           | fine that I've inlined the call, whereas it's important that
           | get_location isn't inlined. But this syntax also suggests to
           | a code reviewer that maybe insert should wrap the error
           | returned by get_location and caught.
           | 
           | Of course you could always catch the error and restore the
           | state, but without the `try` syntax, you can't tell by
           | reading the function which lines can fail, and where you need
           | to handle the error.
        
         | Spivak wrote:
         | But you broke encapsulation because locations and metadatas
         | have an invariant that must be externally enforced. I don't
         | think that meets the standard of well-encapsulated. If
         | locations and metadatas must be updated together then you
         | should only be able to do it via some method update() that
         | takes location and metadata.
        
           | cipherzero wrote:
           | You're maybe right, but does that mean the language should be
           | hostile to those that don't write well designed code?
        
             | nerdponx wrote:
             | I think a lot of programmers would say "yes" to this,
             | enthusiastically.
        
               | gpderetta wrote:
               | Yes. In particular you would want badly designed code to
               | stand out.
        
           | lalaithion wrote:
           | Consider this to be the only function that modifies locations
           | and metadatas. It is the function that is supposed to
           | maintain that invariant. You can pass in location and
           | metadata (or make this a method on an object with those
           | values as private members) without changing my point.
           | 
           | Under the rules laid down by the original poster in the link,
           | functions which only modify well-encapsulated types via their
           | public APIs cannot violate invariants.
           | 
           | My code shows that there are "invariant[s] that must be
           | externally enforced" even in code that only deals with well-
           | encapsulated types via their public APIs. And if you don't
           | have explicit error handling, it isn't obvious that the
           | invariant can be broken.
        
             | Spivak wrote:
             | > well-encapsulated types via their public APIs cannot
             | violate invariants
             | 
             | ... of those well-encapsulated types. Throwing an exception
             | like this cannot break an internal invariant of the list
             | object which is what the author is saying.
             | 
             | You can't break the list object which sounds crazy by
             | modern standards but in languages without this kind of
             | encapsulation (like C) this can and does happen.
        
               | lalaithion wrote:
               | That's a very useful guarantee, I agree, but it's not a
               | good enough reason to not have "try" as a syntax.
        
       | shadowgovt wrote:
       | One of the interesting things (IMO) about programming language
       | design is how the designer can choose to make some concepts
       | harder or easier to express, and how those decisions shape the
       | kind of programs that are easy to write with the language.
       | 
       | Here, Swift (it seems) makes an implicit suggestion that a
       | developer keep their error handling tight and bounded. If you
       | don't, the language softly penalizes you with the need to add
       | keywords to be explicit about changed flow. Go does something
       | similar in eschewing try / catch error handling for a single
       | panic / recover system and very explicit error handling via
       | return values (this was a very intentional design to address what
       | the language creators perceived to be common failure modes in
       | code they were maintaining at the time in other languages, see
       | here
       | [https://go.googlesource.com/proposal/+/master/design/go2draf...]
       | for details). In Go, it's harder to make a certain category of
       | mistake, but at the cost of the user writing more code to handle
       | the error path (and assuming the implicit cost that every line of
       | code could introduce a different mistake).
       | 
       | Every language feature in every language we use (static type
       | declaration vs. implicit types with casting, variable
       | predeclaration vs. implicit creation on first write or read, GOTO
       | vs. function calls vs. try/catch vs. continuation passing, etc.)
       | makes these tradeoffs.
        
         | sharikone wrote:
         | I don't know why but for a moment I read this comment in horror
         | imagining language developers implementing telemetry in the
         | compilers and tooling themselves, then relying on them to
         | "improve the experience" by changing the spec every six months.
        
           | shadowgovt wrote:
           | Oh, they wouldn't have to do that.
           | 
           | ... a scan of GitHub and feeding keywords and constructs into
           | a couple hoppers would be all the ML you needed for that. ;)
           | 
           | (... I'm joking, but if you're a language designer, this is
           | actually not a terrible idea for figuring out how people use
           | your language "in the wild." What you do with that
           | information will separate wisdom from knowledge).
        
           | pjc50 wrote:
           | Microsoft dotnet has opt-out telemetry. I'm not sure for
           | what.
        
         | Buttons840 wrote:
         | What do you think of Java's declared exceptions (or whatever
         | they're called)? They always seemed like a good idea to me, but
         | were ruined by people being lazy or not understanding how to
         | properly design exceptions.
        
           | earthboundkid wrote:
           | With checked exceptions, if today my function can only throw
           | a FileException, then tomorrow I cannot start throwing a
           | URLException without breaking any callers depending on my
           | function having only one possible exception type. The lead
           | architect of C# cited this problem as one of his reasons for
           | not adding checked exception to that language.
           | https://www.artima.com/intv/handcuffs.html#part2
           | 
           | The solution to the problem with to have errors be
           | dynamically typed (a trait in Rust, an interface in Go, etc.)
           | and insist that callers be ready for an unknown error type to
           | bubble up.
        
           | cr212 wrote:
           | some disadvantages:
           | 
           | i) There's RuntimeException which is an unchecked exception
           | which could happen anywhere, so even the absence of a throws
           | is not a guarantee of not throwing.
           | 
           | ii) In order to avoid many different throws specifiers, you
           | end up with wrapping exceptions to a smaller set of
           | exceptions (e.g. JNDI may throw a NamingException that wraps
           | an IOException)
           | 
           | iii) IDEs offer to put try-catch { // todo } to avoid
           | compiler errors. Developers often stop thinking then, where
           | the right thing is more often to let it throw - e.g. I've
           | seen FileNotFound being caught and logged (at debug) and not
           | exposed to a caller, so a UI doesn't see it, with bad
           | results.
        
             | jcranmer wrote:
             | > iii) IDEs offer to put try-catch { // todo } to avoid
             | compiler errors.
             | 
             | This is the thing that always pissed me off. The default
             | catch block should be to wrap it in a RuntimeException and
             | rethrow it.
        
           | shadowgovt wrote:
           | Personal opinion: I think they were a good idea but in
           | practice they don't work great because they try to pretend
           | exceptions work differently from how they really work.
           | 
           | They bump up against a reality of exceptions in a bad way:
           | once you _have_ a system for throwing exceptions, the set of
           | types your code must handle becomes fundamentally unbounded
           | because any of your dependencies can change at any time. If
           | there are no exceptions and you have a function-call control
           | flow, you can enforce that the only types the caller
           | understands from the callee are the types in the signature.
           | If your language supports thrown exceptions, then at the site
           | of a function call, the value resulting from the call can be
           | the values in the signature or any exception that can be
           | generated by any function called by the callee (and in
           | general, without control of the entire code stack, that set
           | is unknowable).
           | 
           | That's a fundamental truth of thrown exceptions, and trying
           | to constrain it by putting the thrown type in the signature
           | left us in the state we're in now... People just throw and
           | catch very abstract exceptions because they'll have to handle
           | those exceptions anyway, since it's impossible to guarantee a
           | dependency won't try to throw them.
        
             | pierrebai wrote:
             | > "If there are no exceptions and you have a function-call
             | control flow, you can enforce that the only types the
             | caller understands from the callee are the types in the
             | signature."
             | 
             | This has historically resulted in either ignoring errors
             | from sub-functions, because their error return type don't
             | map properly, or returning generic "i got an error" without
             | any more information about what the error was.
             | 
             | And all this was historically patched over by having
             | verbose log files that could be written to from anywhere
             | and the user had to peruse and try to make sense of after
             | the fact.
             | 
             | Some franework tried to create complicated error code (like
             | Windows COM) but in the end there is no free lunch. Errors
             | are hard.
        
       | shimzero wrote:
       | Video of Biden giving pallets of dollars to Taliban:
       | https://twitter.com/johncardillo/status/1430851901239795712
       | 
       | Biden has killed dozens of American citizens:
       | https://www.zerohedge.com/geopolitical/us-allies-halt-evacua...
       | 
       | Remove Biden Now!
        
       | floatingatoll wrote:
       | Linking to forum discussions is a bad substitute for a blog post
       | presenting a view, and I can't determine why OP is linking this
       | discussion from last year today. I wasn't able to take away any
       | value from this link because I couldn't read all 153 comments in
       | it.
        
       | shitmonger wrote:
       | It is about time for a good old American revolution.
       | 
       | Most of the White House should be executed on the front lawn.
       | 
       | Milley should be drowned into the Potomac.
       | 
       | Blinken should be thrown out of a helicopter.
        
       | cr212 wrote:
       | It's important to distinguish:                 asset1 = await
       | httpGET(...)       asset2 = await httpGET(...)
       | 
       | from:                 assets = await Promise.join(httpGET(...),
       | httpGET(...))       asset1 = assets[0]       asset2 = assets[1]
       | 
       | Where the first does one request and doesn't start the next until
       | the previous had completed, and the second does them
       | concurrently.
       | 
       | BUT - you could have a language where async functions are awaited
       | by default, and you'd have to write something like:
       | assets = Promise.join([pending httpGET(...), pending
       | httpGET(...)])
       | 
       | Where pending means don't await, and absence of it when invoking
       | an async function does require it.
       | 
       | Presence of an 'async' decorator on functions isn't strictly
       | needed, but does convey extra information in the API, which is
       | useful.
        
         | tomp wrote:
         | > Presence of an 'async' decorator on functions isn't strictly
         | needed, but does convey extra information in the API, which is
         | useful.
         | 
         | I disagree. It's akin to having a `gc` decorator on a function
         | that uses GC. Maybe useful for C++, but not useful for high-
         | level languages like Java, Swift, Go, JavaScript, Python.
         | 
         | Long-term, I firmly believe `async` will reveal itself to be a
         | mistake. It's simply unwillingness by language designers (or
         | more likely implementors) to treat threading like GC - "that
         | magic is OK, but this magic needs to have extra syntax".
         | 
         | Go chose a different route and treats all runtime magic as
         | implicit behaviour (well, almost all - goroutines can block in
         | loops - but they're trying to fix that, or maybe they have
         | already). Java (project Loom) is going down the same path. It's
         | only a matter of time Swift, Python etc. realize their mistake.
        
           | jlokier wrote:
           | > unwillingness by language designers (or more likely
           | implementors) to treat threading like GC
           | 
           | There is a technical reason behind this, it's not just style.
           | 
           | The async-await transformation is highly compatible with
           | calling other things in the C environment and receiving
           | callbacks from that environment.
           | 
           | In some implementations the async-await transformation
           | converts nested async functions to stackless state machines
           | that are compatible with the C environment assumed by other
           | libraries (especially in other languages) and by the
           | operating system.
           | 
           | But it does have some performance cost, compared with
           | functions that can assume stack scope. That cost only appears
           | when using functions labelled "async".
           | 
           | Making every function automatically async in this model is
           | nicer syntax (imho) but adds that cost to every function,
           | unless the compiler is able to do a more global analysis, or
           | deviates from strong compatibility with the C environment.
           | 
           | Another approach is to transform them to system threads. Then
           | you don't need the async-await transform and it's compatible
           | with the C environment, but you lose performance in some
           | types of program where async-await is used, and sometimes a
           | lot of memory or address space. So there's still a cost.
           | Especially on targets where threads aren't particularly well
           | implemented.
           | 
           | Yet another is green threads or other C-compatible coroutine
           | implementation. This costs some compatibility with the C
           | environment on some targets, though. Switching stacks in C
           | works almost everywhere, but not everywhere, and has
           | portability issues. The standard library function to assist
           | with stack switching has been removed from POSIX, but it was
           | never particularly fast anyway.
           | 
           | Go gets around this by being willing to deviate more from the
           | C environment. It goes hand in hand with the very self-
           | contained nature of Go compiled programs. Java gets around
           | this by being a JIT interpreter which is free to do a lot of
           | things its own way inside the Java execution.
        
           | dcow wrote:
           | I believe if you read far enough in the thread you'll see
           | Swift's "Structured Concurrency" proposal does the same
           | thing. In Go you must `go ...` to invoke concurrency. In
           | Swift that would be `async let = ...` under the proposal
           | (which has already been implemented) on main. `async` just
           | says this function returns a "future" and `await` just
           | desugars the future. I prefer Rust's approach where the
           | future is an actual type, but there's a nice element to
           | swift's where it is prt of the syntax, as is similar with
           | swift's optionals.
        
         | jlokier wrote:
         | Or you can have a language where the implied `await` is delayed
         | until the information is used, but the initiation starts as
         | soon as the information required to initiate is ready:
         | asset1 = httpGET(...)       asset2 = httpGET(...)
         | doSomethingWith(asset1)       doSomethingWith(asset2)
         | 
         | No problem, both requests concurrent, don't have to think about
         | it. They will probably be neatly batched by the executor into a
         | single outgoing TCP packet too.                 asset1 =
         | httpGET(...)       asset2 = httpGET(functionOf(asset1))
         | doSomethingWith(asset2)
         | 
         | Second request has to wait until the first has completed,
         | because this is correct. It can't be started until the first is
         | completed.
        
       | jayd16 wrote:
       | Are there any popular UI frameworks/architectures that are
       | actually compatible with implicit context switching?
       | 
       | I see a lot of desire to purge await from languages but I find
       | the explicit control necessary. What would UI code look like with
       | implicit yields?
        
         | metaltyphoon wrote:
         | C# uses "implicit context switch" on its async/await as it
         | captures the current SynchronizationContext by default. On
         | WPF/winforms/UWP and classic ASP there is one, so unless you
         | other wise tell it, the await always comes back on the UI
         | thread.
         | 
         | Console and ASP net core have null as their
         | SynchronizationContext.
        
           | jayd16 wrote:
           | C# will not context switch until you yield execution with an
           | await. There is some default continuation setup that will
           | happen if you use the Task Parallel Library (where the
           | context management is explicit but abstracted away inside the
           | library) but that's a different thing.
        
         | bluquark wrote:
         | Kotlin is used to write UI code, and potentially-yielding
         | methods are called with ordinary function call syntax. Kotlin
         | does not have the "await" keyword, but it does have an analogue
         | to the "async" keyword, called "suspend".
         | 
         | Here is a post from Kotlin's language designer on their
         | philosophy: https://elizarov.medium.com/how-do-you-color-your-
         | functions-...
        
           | jayd16 wrote:
           | Kotlin has context blocks instead of the looser context
           | hopping continuation syntax of C# but it seems like it's
           | trading one explicit syntax for another. You can still get
           | off main thread runtime exceptions in Kotlin on Android, so
           | it doesn't solve the issue entirely.
           | 
           | I think that just shows how necessary some kind of explicit
           | syntax is, despite all the talk I see that it's unnecessary.
           | I would love to see more examples though, or a Kotlin native
           | UI.
        
             | bluquark wrote:
             | Agreed. The only major language I know of where it's fully
             | implicit is Go, which is rarely used for UI.
             | 
             | > I would love to see more examples though, or a Kotlin
             | native UI.
             | 
             | Jetpack Compose is a Kotlin-native UI framework. Here are
             | some examples of how it uses coroutines: https://developer.
             | android.com/jetpack/compose/kotlin#corouti...
        
       ___________________________________________________________________
       (page generated 2021-08-26 23:02 UTC)