Comment by _flux

12 hours ago

I personally don't enjoy the MyObject? typing, because it leads to edge cases where you'd like to have MyObject??, but it's indistinguishable from MyObject?.

E.g. if you have a list finding function that returns X?, then if you give it a list of MyObject?, you don't know if you found a null element or if you found nothing.

It's still obviously way better than having all object types include the null value.

> E.g. if you have a list finding function that returns X?, then if you give it a list of MyObject?, you don't know if you found a null element or if you found nothing.

This is a problem with the signature of the function in the first place. If it's:

  template <typename T>
  T* FindObject(ListType<T> items, std::function<bool(const T&)> predicate)

Whether T is MyObject or MyObject?, you're still using nullpointers as a sentinel value;

  MyObject* Result = FindObject(items, predicate);

The solution is for FindObject to return a result type;

  template <typename T>
  Result<T&> FindObject(ListType<T> items, std::function<bool(const T&)> predicate)

where the _result_ is responsible for the return value wrapping. Making this not copy is a more advanced exercise that is bordering on impossible (safely) in C++, but Rust and newer languages have no excuse for it

Different language, but I find this Kotlin RFC proposing union types has a nice canonical example (https://youtrack.jetbrains.com/projects/KT/issues/KT-68296/U...)

    inline fun <T> Sequence<T>.last(predicate: (T) -> Boolean): T {
        var last: T? = null
        var found = false
        for (element in this) {
            if (predicate(element)) {
                last = element
                found = true
            }
        }
        if (!found) throw NoSuchElementException("Sequence contains no element matching the predicate.")
        @Suppress("UNCHECKED_CAST")
        return last as T
    }

A proper option type like Swift's or Rust's cleans up this function nicely.

Your example produces very distinguishable results. e.g. if Array.first finds a nil value it returns Optional<Type?>.some(.none), and if it doesn't find any value it returns Optional<Type?>.none

The two are not equal, and only the second one evaluates to true when compared to a naked nil.

  • What language is this? I'd expect a language with a ? -type would not use an Optional type at all.

    In languages such as OCaml, Haskell and Rust this of course works as you say.

    • This is Swift, where Type? is syntax sugar for Optional<Type>. Swift's Optional is a standard sum type, with a lot of syntax sugar and compiler niceties to make common cases easier and nicer to work with.

      1 reply →

Well, in a language with nullable reference types, you could use something like

  fn find<T>(self: List<T>) -> (T, bool)

to express what you want.

But exactly like Go's error handling via (fake) unnamed tuple, it's very much error-prone (and return value might contain absurd values like `(someInstanceOfT, false)`). So yeah, I also prefer language w/ ADT which solves it via sum-type rather than being stuck with product-type forever.

  • How does this work if it is given an empty list as a parameter?

    I guess if one is always able to construct default values of T then this is not a problem.

    • > I guess if one is always able to construct default values of T then this is not a problem.

      this is how go handles it;

        func do_thing(val string) (string, error)
      

      is expected to return `"", errors.New("invalid state")` which... sucks for performance and for actually coding.

I like go’s approach on having default value, which for struct is nil. I don’t think I’ve ever cared between null result and no result, as they’re semantically the same thing (what I’m looking for doesn’t exist)

  • Eh, it’s not uncommon to need this distinction. The Go convention is to return (res *MyStruct, ok bool).

    An Option type is a cleaner representation.