Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Rather than extending number-of-return-values overloading to user-defined methods, I’d say just deprecate it altogether. It might have made sense in a world where generic functions are magic and weird and so you want to minimize the number of them (but on the other hand don’t necessarily mind making them even more magic). But in a Go with generics, operations like “access value that may not be present” can just be regular named methods. That applies to both user-defined collections and the builtin ones. Would help pay down the debt...


Aren't these just two different operator[] ?

    value, exists := usertype["something"]
    value := usertype["something"]

    func (t *usertype) operator[](k KeyType) (value)

    func (t *usertype) operator[](k KeyType) (value, bool)
Should work, no?

(EDIT: usertype would be the templated type, so <userType> or whatever the syntax ends up for that. so the above would be the instantiation of the template for a particular type)


If you have sum types, multiple return is less useful.

if you have proper tuples and destructuring assignment, you can entirely remove that abomination.

Right now, it's impossible for a user to define a method that can return 1 or 2 values depending on how it's called.

Only stdlib types, like map and channel, get to do that.

It's fuggin horrific, and spreading that further to user defined functions would be a mistake.


My point is that you don't really know if the stdlib types actually have one method that returns 1 or 2 value or two methods one returning 1 value and the other returning 2 where the compiler chooses based on the call site which one to call. Just because [] looks like one method to the user doesn't mean it has to be one method.

Having two different functions is a way of dealing with this while minimizing change as it's just syntactic sugar and not a language change. Variable length tuple types are a much bigger change. They'd also presumably be a lot more expensive. Ideally a map type implemented by the user should be as performant as the built-in map type.


Tuples wouldn't be more expensive if implemented the way most languages do, where a tuple type has a fixed length and series of element types, e.g. '(Foo, error)'. In other words, structs with some syntax sugar.

But they're the wrong abstraction for this anyway. A better choice would be a type like maybe<Foo> (as mentioned in the post), that only lets you get at the contained Foo if it exists, rather than the current practice of returning a fake default Foo value in the case where it doesn't.

Having two overloaded functions would work, but it would increase the complexity of the language compared to dropping the overloading feature. Yes, for consistency you'd want to make that change to the builtin types as well, and that would be churn. But only in code that's already being churned: if generic collections are added (and maybe some immutability stuff), I think you'd want to inspect just about any code that uses slices or maps to see if a new collection type might work better or better follow the new idioms. (Mind you, I don't suggest actually breaking backwards compatibility, just leaving some of the existing stuff in a permanent supported-but-deprecated state. So "change everything that uses slices or maps" isn't as bad as it sounds - it'd be a recommendation, not a requirement.)


maybe<Foo> or it's C++ equivalent optional<Foo> is more expensive. You now either have a bool + a value, or a pointer that can be nullptr. So you're consuming more memory at the very least and if you want to panic on accessing a value that doesn't exist that means an extra comparison as well. Granted there are situations the compiler can optimize this away in C++. I also find that starting to use optional in C++ code leads to it being used everywhere and for everything which introduces new run-time failure modes that can't be detected on compile time and in general IMO messes up the code.

An alternative is to split find and access into two separate operations like C++ or to provide "in" like Python. Is there any language where a map/set access returns an optional/maybe?

FWIW I agree the current solution is clunky. It's clunkiness was evident prior to the template/generics question :)


I was envisioning that the maybe-returning version would be a separate method, equivalent to the current two-return-value variant, which of course already has that overhead.

Having the default return a 'maybe' would be a possibility; I'm pretty sure the practical performance difference would be completely negligible, given all the other stuff a map lookup has to do, but it might have worse ergonomics. In that case you'd probably want a builtin optional-unwrapping operator like some languages have, so it's not too verbose if you expect the element to be there.

> Is there any language where a map/set access returns an optional/maybe?

Swift is one. It has ! as an unwrap operator, along with other syntax sugar for optionals, so it's not verbose:

      5> let q = ["a": "b"]
    [snip]
      6> q["a"]
    $R2: String? = "b"
      7> q["x"]
    $R3: String? = nil
      8> q["a"]!
    $R4: String = "b"
      9> q["x"]!
    fatal error: unexpectedly found nil while unwrapping an Optional value


A maybe<T> in a language with halfway decent support for sum types is most likely a tagged pointer to a T, if it is reified at all.

A clever compiler is likely to do the same thing for a pair of (bool, T): represent the bool as a tag in the pointer, store the T. If the value is reified at all.

What new run-time failure modes do you get with optional? Is it just what happens when you blithely ignore the "nothing" possibility and attempt to extract the contained value "on faith" ?


How do sum types make multiple return less useful? Multiple return is more like returning a single value of a product type—like a tuple or, less sexily, struct—than like returning a single value of a sum type. What am I missing?


Multiple return is almost always used as an optional/either type construct.

If you have a type for Some | None, then the return type of maps/chans/etc of (T, bool) aren't needed.

Similarly, the (T, err) returns can be replaced by a T | error sum type.

Sometimes multiple returns are used in other cases, but returning a struct covers those, or proper tuples where the value you return is a tuple.

I'm just claiming that multiple return is 90% of the time being used to patch over the lack of proper optional or either types.


The difference in behavior between the one-return-value and the two-return-value version is that the former returns a zero value when the key does not exist in the collection. The one-return-value version is only there, basically, because you can use its return value inside an expression, whereas multiple return values have to be unpacked into variables first (unless you `return` them directly).

If Go had generics, the overloads could be merged into a single operator[] that returns a Maybe<ValueType>. Then that Maybe type would have an Or() method to supply a default, e.g.

  doSomethingWith(collection["foo"].Or(42))
  doSomethingWith(collection["foo"].OrZero())




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: