1. 18
  1.  

  2. 5

    This is a well-written post, but I think I disagree with the author w.r.t. this actually being a problem: effects within closures and iterators are subtle things, and I prefer Rust’s approach of requiring users to explicitly ferry them into the surrounding context.

    It also tends to be only a minor hiccup in practice: collect can be used to flatten a sequence of Result<T>s into Result<Container<T>>, which is the intuitive and intended behavior in almost all circumstances.

    1. 1

      Oh cool, I didn’t know about this. Can it do the same with an iterator of Future<T>s into a Future<Container<T>>?

      Actually thinking about it, I guess not, because there’s a choice to be made about whether to do them all in parallel or in sequence.

      1. 1

        I know this thread is long dead, but you actually can do this with join_all.

        Or you can use FuturesUnordered to pull out results as they complete. Unfortunately it’s a little weird to use: iterating returns a Future to await, which in turn returns Some(_) or None to signal end of iteration. An Iterator needs to return Option, not a Future<Option>.

        let mut results: FuturesUnordered<_> = ...
        while let Some(result) = results.next().await {
            ...
        }
        
    2. 3

      I really like this article. Learning Rust from a Haskell background, I’ve been coming across the annoyance of not being able to use ? inside a closure, and thinking about it as a consequence of Rust not having monads. I hadn’t thought about the TCP-preserving-function style at all.

      I tend to agree with the author though that it seems unlikely that Rust will address this problem any time soon. And to be honest, I’m alright with that. I think given that Rust only has the three effects mentioned (or four if you count memory ownership as an effect), it’s an at worst “OK” solution to simply combine them ad-hoc (like Iterator::map_while or tokio::StreamExt::map etc.). It works OK-ish for TypeScript.

      1. 2

        It’s not clear for me what the author means when saying:

        Rust’s design philosophy has landed on the principle that all effect points should be marked in the program source, including forwarding, so that users can more easily reason about the control flow of their program.

        That may be true for exceptions (which Rust abandon completely), but there are more (e.g. divergence, heap allocation, etc.) kinds of effect which have nothing with control flow.

        Rust has an excellent system for managing effects related to mutation and memory: this is what ownership and borrowing is all about, and it beautifully guarantees soundness even in the presence of concurrent mutation.

        IMHO, this is not true, Rust does not manage effects for mutation and memory, it rather forbids that with ownership and borrowing. Consequently, its type system is limited, e.g. (co-)recursive types cannot be represented naturally in Rust.

        1. 4

          Genuinely curious, what do you consider unnatural about recursive types in Rust? Rust’s lack of implicit boxing means you need to wrap the recursion in Box, but that’s what happens under the hood in other languages anyway. Perhaps there are forms of recursive types which you’re thinking of, but I’m uncertain what they’d be.

          1. 1

            I’m not sure that other languages use box (or reference, etc) to implement recursive types at low-level, but that’s irrelevant since we are talking about the expressiveness of the type system. For example, one way to represent a list type in Rust is:

             enum List<T> { Cons(T, Box<List<T>>), Nil }
            

            First, this type is not a representation for the functional definition of a list (i.e. Fix(\L.Nil + Cons(T, L)). Second, since an argument about a infinite size of such a type, at a “high enough” level as type system, why we need to care whether a type is located on heap (since box) or not: the type checking should proceed before deciding where some variable of this type is stored.

            Come back to the article, I’m quite confused about “effect”, e.g.

            Falliblity: The effect of a section of code failing to complete and evaluate to its expected value (in Rust, think Result)

            Result is not effect, it’s a type, as the language has no support to reason about “a section of code failing to complete”. Using Result just helps forcing the programmer to rest in the Ok(s) railway, but this has nothing about effect.

            The article said about using monad, as in Haskell. but note that Haskell does that because it is essentially has no support for effect (then it has to “simulate” effect by the type system). We may note that effect checking is not the same as type checking, e.g. suppose a function f : () -> <E> T where <E> is the notation for an effect E, and g : T -> <E'> T' then g ° f should type check (by a language supporting effect) since it’s not always g which has to handle the effect E.