The obvious final step
akrzemi1.wordpress.com
akrzemi1.wordpress.com
The Lisp crowd was right, as usual.
40 years ago.
Fortunately we’re getting there, feature by feature. Maybe 2030 will be the decade yaml goes away.
Such "contexts" are somewhat ad hoc in Lisp, whereas Ruby and Python have built-in language support and special syntax for them. The traditional Lisp idea is that you don't need special language support when you have macros and higher-order functions, but the special language support also means that all context managers follow the same protocols, whereas ad hoc contexts introduced by CALL-WITH-FOO/WITH-FOO are more like a common loose convention than a standardized system.
Functional programmers have also understood this idea of structured contexts for a very long time, giving us Functor, Applicative, and Monad.
https://en.wikipedia.org/wiki/Advice_(programming)
> all context managers follow the same protocols
Lisp is an old language with lots of dialects and systems. Thus people had different needs for context management and different facilities available to implement it.
Absolutely, my first thought was `dynamic-wind`.
https://www.gnu.org/software/guile/manual/html_node/Dynamic-...
Declaring data imperatively is quite a contradiction. You need a lot of effort and misguided design to create it. You don't need first class code blocks to avoid it.
(But, of course, first class code blocks help in many other ways.)
A big part is those good solutions are those little lambda-receiving functions that do all the heavy lifting without imposing a lot of boilerplate. Not too many of them, of course.
IMO this kind of thing is where the “getting things done” and “good architecture” meet.
with open('file') as myfile:
process(myfile.read())
When control leaves that context, `file.close()` is always called. You don't have to remember to close it. Even if `process()` raises an exception, the `with open` context will close it before passing the exception upward. You never have to remember to clean up the file yourself.It's not that you can't do all that without context managers, obviously. People have done it for a long time. It's just that the `with open...` pattern is effectively bulletproof and removes just one more potential footgun.
open('file', (myfile) => process(myfile.read()))
It would be on the implementer of `open` to call `file.close()`, but presumably that's true of python as well? def open_context_manager(filename):
try:
f = open(filename)
yield f
finally:
f.close()Specifically, the XML element example involves ‘pushing’ the context of a new element into the document - a context which MUST be ‘popped’ before continuing.
This problem is literally why GOTO was considered harmful, why old school programmers insist that subroutines should have one return statement, why functional programmers are right to despise mutation, and why iterators are preferable to loops.
So as a programmer, you shouldn’t be surprised that your language of choice has clever ways to solve this problem, or that you can think of a dozen ways of avoiding this. It’s like the ur-problem of writing code.
Maybe because first-class support for lambdas was added too late in C++ and Java, or maybe because it was always PITA to type.
Ruby syntax is the lightest there could be (and it supports both {braces} and do/end keywords)
transaction do
foo()
bar()
end someTransaction = do
incrementCounter
writeFile "foo.txt" "bar"
decrementCounter
I get this: src/Lib.hs:11:5: error:
• Couldn't match type ‘IO’ with ‘STM’
Expected: STM ()
Actual: IO ()
• In a stmt of a 'do' block: writeFile "foo.txt" "bar"
In the expression:
do incrementCounter
writeFile "foo.txt" "bar"
decrementCounter
In an equation for ‘someTransaction’:
someTransaction
= do incrementCounter
writeFile "foo.txt" "bar"
decrementCounter
|
11 | writeFile "foo.txt" "bar"
| ^^^^^^^^^^^^^^^^^^^^^^^^^Kotlin can have exactly the same code with lambdas, using either a receiver type, or a context receiver. And it's type safe.
transaction {
foo()
bar()
}
Type-safe DSLs become a pleasure to build and use.It’s something you’d get with a linear type system (use values exactly once), but they’re not present in any particularly popular languages. I suspect Haskell might be able to achieve roughly this, but I’m not certain.
The closest I know definitely about is Rust, which has an affine type system (use values at most once). You can mark types and functions with the #[must_use] attribute, which says that you must do something with values of the type or returned from the function at least once, or it’ll produce a compiler warning. But what’s needed in these cases is that you must consume the value. I have often wished for this in Rust, but I doubt it would be accepted into the language (and so haven’t tried proposing it formally) because of its comparatively niche usefulness due to the interactions with destructors and panic unwinding. (Basically, you couldn’t actually rely on it for resource management: if you wanted it for something like “end database transaction, but actually do something with an error code rather than silently dropping it like a destructor”, you’d still actually need to implement a regular no-return-value destructor, so is requiring that the user write `tx.commit()?;` actually useful, given that a panic before that line would lead to effectively `drop(tx);` happening instead?) For most purposes, #[must_use] is good enough. But it still disappoints me, because I think most places that #[must_use] gets used, they’d actually be better suited by a #[must_consume]. Hmm… maybe I should contemplate proposing it after all.
But yeah, the lambda approach is often a good solution.
EDIT: I found this:
"Basically, the typestate pattern in Rust today provides a way to ensure that state machines are used correctly, but does not fully defend against them being unused, or discarded before completion." [1]
So I guess #[must_use] is not enough and that's what you were saying in your comment already.
EDIT 2: I found another good write-up of this topic:
"The Pain Of Real Linear Types in Rust" [2]
[1] https://users.rust-lang.org/t/write-up-on-using-typestates-i...
#[must_use]
struct Transaction;
impl Transaction {
fn new() -> Self { … }
fn action(self, …) -> Self { … }
fn commit(self) -> Result<(), …> { … }
}
let mut tx = Transaction::new();
tx = tx.action(…); // ← `tx =` is the difference.
// (Any panic at this point will roll back the transaction via Drop,
// but any errors in the process will be swallowed or abort or who knows what, there’s no standard.)
tx.commit()?; // unused-must-use warning on the last assignment of tx (`tx = tx.action(…);`) if this line is absent
A hypothetical #[must_consume] would let it be like this: #[must_consume]
struct Transaction;
impl Transaction {
fn new() -> Self { … }
fn action(&mut self, …) { … }
fn commit(self) -> Result<(), …> { …; drop(self); Ok(()) }
}
let mut tx = Transaction::new();
tx.action(…);
// (Same panic behaviour.)
tx.commit()?; // unused-must-consume warning on the last assignment of tx (`let mut tx = Transaction::new();`) if this line is absent
With putting the state into the type, you’d be limited to annotating your entire generic type with #[must_use], not individual states. (#[must_use] won’t propagate through generic parameters, nor should it.)The destructor approach has one advantage for semantic clarity: it’s guaranteed that your execute-within-context block really executes immediately and just once. Passing a lambda to a function means the function could call the lambda now, or later, or ten times, or maybe never. Still, I think it’s a more readable approach for contexts that are not holding resources.
Oh, a monad.
Yeah, a monad solves it. But it's quite overkill.
The article went from destructors to the with-pattern. Once you start thinking about exceptions, you might reach for try-with-resources. With Async exceptions you might look at bracket. If you want composability, go for ResourceT.
The original API goes out of its way to be dangerous and hard to use.
For example in the first example from the article calling xml.attribute doesn't make sense without a prior xml.begin_element.
By using the Typestate Pattern the compiler would ensure that you can only call xml.attribute after a xml.begin_element. Of course all of this only works in cases where the order of steps is fixed at compile time.
EDIT: Please read chrismorgan's comment why, at least in Rust, this does unfortunately not really solve the "Obvious Final Step" problem.
Conceptually, a function implementing the body of a database transaction should be idempotent. If a language had a function type that did not mutate global or closed-over variables and only affected the world by calling methods on its parameter, it would be difficult to mess up.
Haskell can do this.
Rust’s Fn(&mut txn) -> Ret is kind of close, but it doesn’t express that the function may not have mutable captures, and it does nothing about interior mutability or calling impure functions.
with xml.element("port") as e:
e.attribute("name", name)
e.attribute("location", location)
And that's itAnyone else immediately think about lambdas and surprised by this zinger at the end? I'll grant that in some languages lambdas are harder to debug, but I don't find the example more difficult to read or reason about _at all_.
y = foo() {
bar(it)
}
is just pretty syntax for y = foo({ it -> bar(it) })https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n41...
Sometimes there's a different way to express the same thing without losing any meaning, but which might be more terse or robust.