6 ms·
go suffers from this problem. Lack of generics makes it impossible to define an optional<T> type, and *T is annoying to use and requires an extra allocation. I
by assbuttbuttass 5y ago
go suffers from this problem. Lack of generics makes it impossible to define an optional<T> type, and *T is annoying to use and requires an extra allocation.
In practice, people use a zero value to indicate that a value is missing, but this only works if the zero value isn't also a valid input.
Hopefully the coming generics will resolve this pain point.
- SQueeeeeL 5y agoOne philosophy is that zero input basically never be a valid input unless you're taking in integers. https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/ https://www.infoq.com/presentations/Null-References-The-Bill... . The idea is to try and never create null objects. I think it's interesting and helpful in practice to check/fail early.
- samhw 5y ago> The idea is to try and never create null objects This is one of those things which sounds great in practice, and survives approximately 17 seconds of working on a real codebase.
- SQueeeeeL 5y agoYeah, I think this might be a tab/spaces mixing situation like python had, where it had to be restricted by the language compiler because it's just so tempting.
- simiones 5y ago> One philosophy is that zero input basically never be a valid input unless you're taking in integers This is obviously not possible, since you can imagine lots of complex objects are all integers or themselves composed of integers, and for this entire class of objects, the zero value usually makes sense. For example, a 3-vector (in the physics sense) is a 3-tuple of integers `type Vec3 struct {x, y, z int}` , `Vec3 {0, 0, 0}` being the origin. How does a function specify that it can take an optional 3-vector? Even worse, you can imagine a struct for a potenitally 0-volume cube represented as 8 Vec3s, one for each vertex, whose 0 value is again a valid object. Of course, you could gratuitously add an `IsValid bool` flag which must be true to the class, but the cube example shows how this quickly becomes annoying and bloated.
- SQueeeeeL 5y agoIn this case, the memory pointer for Vec3 would not be null? It will be a defined memory address which points to 3 zeros. I think it's a difference of objects and primitive data types.
- simiones 5y agoThe discussion was that in Go, given the current lack of generics, you have two options to have an optional parameter: 1. Take a *Vec3, using nil as the "optional is missing" value. 2. Take a Vec3, but consider some value as "invalid", often the 0-value as possible. 1 invokes exactly the issues in Hoare's essay. 2 doesn't work for all types, as some types don't have any bit pattern that are not useful, this is what I was trying to point out.
- SQueeeeeL 5y agoI can't think of any situations where (1) would be insufficient, but I'm probably wrong. But I agree that setting an isValid bit for every object is overkill.
- saghm 5y agoI think the point is that it would be nice to be able to express the idea of a value not being present without having to risk accidentally trying to dereference a nil pointer. That's what Option brings to the table, although arguably Go could just provide an implementation of that like they do for slices and maps without actually having to introduce full generics to the language.
- chakkepolja 5y agoapart from what sibling comment mentions, declaring as pointer will often result in a heap allocation in Go.
- simiones 5y agoWell, in the link that you shared, Tony Hoare explains in quite some detail why having support for null pointers is not great. Additionally, a *Vec3 is more than `Vec3` or `null`. For example, the following code will behave very differently based on whether optionalVec is Vec3 or *Vec3. optionalVec := Vec3{0, 0, 0} foo(optionalVec) fmt.Printf("theVec: %+v", optionalVec) // would print {0, 0, 0} with an Optional[Vec3] type // will actually print {2, 0, 0} with *Vec3 ... func foo(optionalVec *Vec3) { if optionalVec != nil { optionalVec.x += 2 //we use a different origin } [...] }
- Jtsummers 5y agoBut many types do have a "natural" zero value, that you may want to use, beyond just the numbers. Consider the empty string, empty list, empty dictionary, empty set. Those are zero values for their types. By reserving them as pseudo-nulls you lose the ability to distinguish between an object which happens to be empty and an object which is meant to mean "null". This is where Optional (or similar mechanisms) come in handy. If you actually want to avoiding creating null, Optional is a cleaner and more consistent way to accomplish this without needing to magic up a null-value in your type's valid set of values or create a one-off Optional (MaybeDictionary, MaybeList).
- saghm 5y agoIt's even worse than that in my opinion: the zero value for maps and slices is actually nil even though they're not pointers, meaning the empty values are not the zero values. This lets you do "fun" things like this (my memory of the syntax might be slightly off): ``` var map1 map[int]int = nil var map2 map[int]int = &map1 var map3 map[int]int = nil ``` Interface types also can be nil even if they're not explicitly pointers because heap pointers are needed under the hood due to the size of the value not being knowable at compile time. You can pretty quickly get into some hairy situations trying to ensure that a value isn't nil when combining pointers or maps/slices with interfaces. I really wish that separating the concerns of present versus not present from value versus reference was more mainstream. I understand why this was traditionally the case for lower-level languages, but even higher-level ones like Java are mostly based on the paradigm that only references can be "not present". This irks me in a similar way to how earlier versions of Java wouldn't allow default implementations of interface methods and instead requiring abstract classes, which classes could not extend from more than one of.
- wahern 5y ago> people use a zero value to indicate that a value is missing Go supports returning multiple results. Maybe I don't read enough Go code to understand what's actually popular, but AFAIU the idiomatic way to accomplish this in Go is something like: if v, exists := lookup(key); exists { do(v) } or if r, err != foo(); err != nil { do(r) } The compiler, unfortunately, can't enforce proper conditionals. OTOH, unlike C compilers Go won't make any dangerous assumptions about dereferencing nil pointers, which is something. I'm not sure Generics can help here. What you really want is compiler enforcement of comprehensive condition checks. For example, via pattern matching switch statements. That's easiest to do with optional types as the unwrapping operation creates a simple point for static constraint checking, but in principle the same thing could be accomplished with annotations on multi-value return types that describe the association.
- assbuttbuttass 5y agoI'm talking about optional inputs, you're right that multi-return works for optional outputs. How would you write a function that takes a string, but can use some default if none is provided? Common way: func f(s string) { if s == "" { s = "default" } ... }
- vishvananda 5y agoYou could always use a pointer for this, but admittedly it is pretty ugly when compared to a true optional type: func f(s *string) string { ret := "" if s == nil { ret = "default" } else { ret = *s } return ret } func main() { s := "foo" fmt.Println("Hello, " + f(&s)) fmt.Println("Hello, " + f(nil)) }
- kubb 5y agoGenerics will let you define an Option[any], just hope that your code reviewers don't decide that this isn't goey, and you should "keep it simple" or "just use a pointer" or something in that vein. There won't be overloading * for getting the value inside and overloading bool conversion to check if the value is there. Still, it's an improvement.