I would avoid using nothing as a value at all costs but I suppose if things ever get meta enough that nothing becomes a value, I would have no choice but to wrap them in Some.
Now that I’m looking at this, I think I do agree that these methods shouldn’t exist. Rather than having 1 canonical method for unwrapping an unfixed number of single-value wrappers (whether in a type parameter of a iterated type-union or in a field of 1 type), it makes more sense to have 1 canonical single-value wrapper so that everything is compatible, not just the unwrapping. Val exists precisely so people don’t define their own versions for compile-time calculations in APIs that require unwrapping and rewrapping to interact. Despite Some{T} technically fitting the bill, an immutable single-field wrapper isn’t really used like Val is because the value itself serves just as well for runtime calculations; Ref is useful because it can add mutability. (And the ship has sailed on Some{T} being used as a scalar-izer for broadcasting, though a 1-element tuple seems just as good and less to type anyway). When a specific context really requires a specific wrapper type or union, it should have its specific unwrapping method that does not interact with the more widespread Val or Ref.
What would alternatively be useful is to create not a single function to “unwrap anything”, but to create a function for each of the common wrappers listed in this thread. The point is to create these functions with “standardized” names, either:
unref(Ref(x)) == x, unsome(Some(x)) == x, unval(Val(x)) == x and so on;
or un(Ref, Ref(x)) == x if going for a single function that remains explicit;
or inverse(Ref)(Ref(x)) == x if basing on the existing interface in InverseFunctions. This can actually be added to that package right now, I think.
Currently there’s a mix that each wrapper is unwrapped very differently, and hard to guess unless you know the specific type interface. And Val doesn’t have an unwrapper at all.
We already have Ref(x)[] and something(Some(x)), introducing another way to do the same thing would only confuse people, it’s not worth the aesthetic symmetry. Not sure how unval would be useful in practice because type parameters of all types are already unwrapped by where clauses of a method. I suppose it wouldn’t be too different from ndims or eltype for AbstractArrays, which are useful interactively.
The comparison with AbstractArray’s parent is apt here. That’s another function whose meaning feels like it’s clear but is impossible to use generically. And by generically, I mean doing something — anything — meaningful with the result of parent(x) without knowing what type x was.
Yes, perhaps it’s an ergonomic convenience, but it’s also quite the footgun.
As a concrete example, in SVD2Julia.jl I define a const Option{T} = Union{Some{T}, Nothing} type because I need to be able to express optionality in the structs of the package. I could of course duplicate that information and have isdefined or boolean flags everywhere, but that robs me of type safety in case the field truly is not set; and I can’t change that fact, I need to be able to say “there is nothing here” (during parsing, e.g. here), because the underlying specification the package is implementing has this kind of optionality built in. Solutions other than some kind of Option{T} make the code much larger, much more difficult to maintain and much less resilient against bad input (in fact, using that Option{T} has already helped prevent bugs in the package!).
Yes, that is a good comparison. To make this maybe a bit more concrete, unwrapping a Ref and then “rewrapping” it into a new Ref won’t work for the cases where Ref can/should be used (not to mention that you have to know the wrapper was a Ref in the first place!). By unwrapping & rewrapping you’re losing the semantic “this came from somewhere else, and must continue to refer to that other place” that is inherent to Ref, which is not shared by things like Some or Val.
I think your requirement forces the following implementation, and it seems quite useless to me?
struct NotASingleItemWrapper end
struct NullPtrError end
struct Rewrapped{T}
item::T
end
struct Primitive{T}
item::T
end
const Rewrap{T} = Union{NotASingleItemWrapper, NullPtrError, Rewrapped{T}, Primitive{T}}
@generated function universal_rewrap(x::T) where T
if isprimitivetype(T) return :(Primitive(x))
elseif length(fieldnames(T)) != 1 return :(NotASingleItemWrapper())
else return :( (isdefined(x, 1) ? Rewrapped(getfield(x, 1)) : NullPtrError() ))
end
end
(edit: fixed language, sorry for being obnoxious the first round)
Code is more likely to look like extract(x). That’s a big loss in readability compared to something(x). With the latter I know what kind of thing x is and what’s going on.
Ref, Some and Val are very different features, that are used in completely different mechanisms. They all “wrap” in some sense, but it seems like a superficial commonality to me. I think it would be a bad idea to base an abstraction on that, especially in Base.
I’m also unsure it’s a good idea from a “pedagogic” perspective, to make users think that unwrapping these values is the same kind of operation. If discoverability is an issue we should rather improve the documentation of Ref, etc.
Maybe I’m missing the big picture, would you have a concrete example of where this would be useful?
To me getting the value from a single value wrapper is mentally always the same wish - but depending on which type I am currently working with (for whatever convenience the type brings), I need to search for the individual function or attribute this very type has for unwrapping.
It is not superficial, it appears often enough that I am opening this discourse issue.
Maybe julia is not the right language for people who think this way. Multiple Dispatch means that calling the same function/api usually corresponds to different operations being run depending on the input type.
I don’t see the footgun. What can be bad? You have a general api to get the single value from the single-value wrapper which people get to know and simplifies their life because they don’t need to search and again for the method used for Ref vs Val, vs WeakRef, vs Some, vs custom types (e.g. SingletonIterator might be helpful).
This seems to be the best way so far - just let everyone themselves define such a function locally. Unsatisfying, but probably the best workaround so far.
Hard disagree. Multiple dispatch means that you can easily extend existing functions with new types and also easily extend existing types with new function(ality). It does not mean that we’re free to invent new semantics for what a function means.
I agree you should have some common understanding for an API, that helps the community. It is not a hard requirement in Julia, but it is very helpful, I agree.
This common understanding is also given here (“extracting a single-value from a single-value wrapper”, or the slight generalization of “extracting a canonical value from a wrapper”).
It is just natural that the same “semantics” (== common understanding) is realized in different ways for different types. (I guess the best Base example is map)
The point is that you can only meaningfully unwrap something if you know what that wrapper is doing. And if you already know what the type of the wrapper is, then you can use the wrapper-specific methods/accessors/etc for it.
This is different from map because you can meaningfully call map without knowing the function or collection type — and there’s an understanding that you should (roughly) be able to do the same sorts of things with the output as you could with the input. It’s not perfect, but there’s a clear desiderata against which bugs may be filed.
That’s not to say there’s not an ergonomic advantage to sharing the same vocabulary, but that’s slightly different than what a “generic function” should ideally be.
I would be immediately confused because extracting a value from a field of Ref is not the same thing as extracting a value from a type parameter of Val to me.
Even if I do accept the broader umbrella purpose as 1 thing by 1 generic function, let’s call it extract here, it can’t be considered in isolation. It’s not any more valid than a generic function that, say, retrieves the 1st parameter of a parametric type’s instance; let’s call it firstparam. Now extract(Val(1)) does the same thing as firstparam(Val(1)), and firstparam([1]) does the same thing as eltype([1]). It’s very easy to create a bunch of overlapping features until there’s no clear 1 way to do each thing, so features should not exist just because they are convenient in isolation, they should also be needed. I don’t think we need 1 thing to unwrap Ref, Val, and all the other context-specific wrappers because they aren’t intended to intermingle e.g. I can’t substitute Ref into foo(::Val{T}) where T = T ... nor should I substitute Val into bar(t::Ref{T}) where T = t[] ....
My aim with this discussion is not to add a function to julia which is needed (whatever this should mean - probably a bugfix or so is a good example). My aim is indeed to simplify the api/interface on these types so that Julia gets indeed more convenient.
It is very important to assess whether something is really convenient for most, or only for some and too confusing for others so that in total it is not a good idea to add it.
But if it is convenient for most, then I would indeed like to add it to Julia.
You may know what the wrapper is, but it’s still a bit of a mental overhead to remember all those different unwrapping functions. That’s why something uniform but still explicit can be reasonable – like un(Ref, x).
I dunno, but I still have it out for parent after it led me down some very deep and dark rabbit holes. The existence of these functions can make it seem like you’re writing generic code (when it’s really not doing what you meant) or can entice you into futile attempts at it.
IMV the place to standardize the parent language is in the field name. Then you know you’re doing something very particular to the type through field access, but still have a common vocabulary. Perhaps the same is true here, but with .value?
For parent and arrays specifically, field access is often inconvenient – array types can and do define their custom properties, so that getfield(x, :parent) would be needed to actually access the parent field. For wrappers it can be less of an issue, and even Val(x).value is possible through getpropery overrides…
My rule of thumb is that fields are implementation details of individual Types and don’t belong to generic APIs… but this is just gut feeling
(I guess I am thinking that it is harder to support a field based api instead of a function based api, maybe I am also afraid of performance penalties when using getproperty)