From late-bound arguments to deferred computation, part 2
From late-bound arguments to deferred computation, part 2
Posted Aug 27, 2022 0:04 UTC (Sat) by marcH (subscriber, #57642)Parent article: From late-bound arguments to deferred computation, part 2
I always thought "core" features of a language like lazy evaluation must be _all_ designed from the start, as a consistent whole. I'm very far from a language expert but my gut feeling tells me that bolting on something that fundamental that late in the game is doomed to fail in various ways.
Posted Aug 27, 2022 0:52 UTC (Sat)
by NYKevin (subscriber, #129325)
[Link] (3 responses)
1. Everything is implicitly lazy all the time, and order of evaluation is totally up to the implementation. For this to work, the entire language has to revolve around it, like Haskell does. Everything has to be referentially transparent and pure, you have to have some sort of "escape hatch" for I/O (which may or may not end up looking like a monad, but it had better exist, unless your language is really domain-specific like CSS), and so on. If you don't do those things up front, then you're going to end up back-solving for them after the fact, and your language is going to be really painful to use.
[1]: https://docs.djangoproject.com/en/4.1/_modules/django/uti...
Posted Aug 29, 2022 11:08 UTC (Mon)
by tialaramex (subscriber, #21167)
[Link] (2 responses)
I don't really see how?
The C++ implicit constructor behaviour means this can be invisible (and hence surprising, and hence a footgun) but it is still a constructor.
Suppose I have types FinalScore (a simple named pair of integers) and SoccerMatch (potentially sophisticated modelling parameters for a game of football). In C++ we can write a function which takes the names of two teams as string_views, and then a FinalScore, but pass it parameters for two strings, and a SoccerMatch. The compiler will implicitly construct the string_views, and the FinalScore, calling a constructor on FinalScore which takes a SoccerMatch (if there isn't one this code doesn't type check).
It does this when the function is called, even if the function implementation actually checks the team names and discards any matches not involving Liverpool without using their FinalScore.
Rust's From<T> (and its cousins Into, TryFrom, and TryInto) are explicit, so you'd need to call the from() function (or the into() function etc.) to actually invoke the thunk. You could once again do this for function arguments, but now it's explicit what's happening (we might just as well call SoccerMatch::simulate here) and you still don't get lazy evaluation.
Rust does have some silent conversions as traits, but they're explicitly not intended to be expensive. Deref and DerefMut are intended solely to make smart pointers work as you'd want them to, and you're cautioned not to do something silly (they're safe Traits, so this can't make your program unsound, but e.g. misfortunate::Double deliberately has a silly implementation of these Traits and it hurts your head) AsRef strongly cautions you that it's intended to be cheap. For example AsRef allows us to take a String (which remember is just a Vec of bytes but we've promised it's UTF-8 text) and get a read only view into those bytes. Cheap.
Posted Aug 29, 2022 17:55 UTC (Mon)
by NYKevin (subscriber, #129325)
[Link] (1 responses)
You say "constructor," I say "function that happens to initialize some memory as a side effect." Either way, it's an implicit function call.
> It does this when the function is called, even if the function implementation actually checks the team names and discards any matches not involving Liverpool without using their FinalScore.
That's not really what I'm getting at. Your function could take SoccarMatch instead of FinalScore, and then call a private helper function which takes FinalScore if and when it actually wants to evaluate it. You *choose* to put FinalScore at the API boundary, which may make sense for your particular application, but C++ doesn't force you to do it that way. You could even do it both ways using templating... which is exactly what Rust's From<T> facilitates (i.e. you could accept a From<T> as an argument, then pass it as a T to some other function, and never worry about the conversion at all).
> Rust does have some silent conversions as traits, but they're explicitly not intended to be expensive. Deref and DerefMut are intended solely to make smart pointers work as you'd want them to, and you're cautioned not to do something silly (they're safe Traits, so this can't make your program unsound, but e.g. misfortunate::Double deliberately has a silly implementation of these Traits and it hurts your head) AsRef strongly cautions you that it's intended to be cheap. For example AsRef allows us to take a String (which remember is just a Vec of bytes but we've promised it's UTF-8 text) and get a read only view into those bytes. Cheap.
I never said this was a Good Idea, and in fact specifically called out that it might not be. Nevertheless, it is a thing that you can do if you really want to.
Posted Aug 31, 2022 1:58 UTC (Wed)
by tialaramex (subscriber, #21167)
[Link]
This is one of those mirror image situations, you should implement From<SoccerMatch> on your FinalScore type, but you don't write From<T> or From<SoccerMatch> in the function signature which eventually might need the score, you write what you wanted, Into<FinalScore> instead. We're going to have a line like:
score: FinalScore = Into::into(some_parameter);
... so you can see some_parameter's type needs to be Into<FinalScore> to match.
This is idiomatic for the iterators, hence IntoIterator but it would be unusual to ask for Into<FinalScore> rather than just FinalScore unless you'd specifically designed all of the API this way. As I understand it, what the deferred computation people want for Python is a type which does not require you to be explicit up front in this way.
And that's why I don't think either C++ or Rust fit the bill. They can explicitly do this, but they aren't really designed to be able to implicitly defer computation, everybody involved needs to agree up front to this arrangement.
From late-bound arguments to deferred computation, part 2
2. Explicitly constructed and implicitly evaluated thunks. For this to work, you probably need some kind of mandatory static typing, so that the compiler can disambiguate between "I want to invoke the thunk now" and "I want to pass the thunk along to somebody else." You can probably do this sort of thing with C++'s implicit single-argument constructors (and, I would imagine, with Rust's From<T> trait), but that doesn't necessarily mean that it's a Good Idea. Python supports a limited subset of this functionality using the descriptor protocol (see for example some of the Django code [1]), but implicit evaluation is restricted to foo.bar expressions, where foo has already been eagerly evaluated at least to some extent. The lack of static typing means that there are all sorts of weird things that can happen if you try to introspect foo or take it apart when bar has not yet been evaluated.
3. Completely explicit thunks. In other words, lambdas (as well as related stuff like functools.partial). Python already has these, and they work reasonably well. There's not much to improve here (people complain about the lack of statement-lambdas, but that's just minor syntactic salt, and you can use a regular def to get around it).
From late-bound arguments to deferred computation, part 2
From late-bound arguments to deferred computation, part 2
From late-bound arguments to deferred computation, part 2