[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)