I am stumbling upon that there does not exist a unified method to unwrap a singleton container. This is a feature often wanted, but Julia misses this so far. Some types don’t even have a single unwrapper.
I want to suggest to support get as the standard unwrap method.
Standard Base types I know of which fall into this confusing category:
Ref has getindex as unwrap, but does not support get
Some doesn’t have any unwrap method as far as I know (EDIT: as commented, something unwraps it)
EDIT: WeakRef was mentioned in the comments - it apparently does not have an unwrap method
… are there others?
Options for unwrap method
get - would be my personal favourite
getindex - currently used for Ref, but not for Some, and on the Val site there was the comment that it could confuse people that Val(1)[] would work, but Val{1}[] not (because of visual similarity of the syntax and semantic similarity of Val(1) and Val{1})
EDIT: only was mentioned in a comment - this unwraps Ref already, as well as Vectors with a single element. Fits very very good to the general idea, and is already established.
another new method like e.g. unwrap - would introduce a new API obviously. EDIT: a name commonly used for this concept is extract as commented below.
Because of the mentioned reasons I would argue that we should add get definitions to all the singleton types like Ref, Some and Val and regard get as a standard API for unwrapping singleton wrappers.
What are your thoughts?
Personal Note: I am also fine with adding both get and getindex - the confusion argument is not really crucial from my view, as people who are using Val are anyway already more advanced in their Julia usage and will know about the pitfalls when confusing Val{1} with Val(1).
get is currently always associated with a key & a default; this explicitly requires no key, and theres’s no mention of a default, so I’m inclined to think this concept is a bit different to get.
I like to follow functional standard namings. And as there is so many opinions with the other names, maybe it is really the easiest and most straighforward.
I like to add extract as new API to julia which is intended to have co-monad like semantics.
But what does unwrapping really mean? Some is intentionally special with its something. You shouldn’t “generically” unwrap a Some — it’s entire reason-for-being is to tell you that it’s not nothing.
I guess this is about the most meaning you can get - supporting the extract part of a comonad. That is the technical meaning of course.
The intuitive meaning is luckily also captured in the name extract and its type MyFunctor{T} -> T, namely that there is a way to extract one element from the given instance of MyFunctor.
If people don’t like these abstractions, they of course can define their own definitions similar to extract, or use more individual methods which better capture the specific meaning of what extract would mean for their context. something is not a good example I guess - because it has more features than a simple unwrap. A maybe better example is fetch, where almost everyone will prefer using fetch instead of extract.
I like this as well - get is for me the most intuitive name in julia for the comonadic extract.
Making it default to getindex with no args unifies these intuitions (as for Ref getindex is already defined).
I wouldn’t do the reverse probably, because indexing syntax is slightly less self-explaining as get and hence rather a candidate for explicit definition.
Of course, in addition, this would require definitions for either get or getindex for Some and Val to accomplish my final goal.
I want to note that you really don’t want to ever just “unwrap” a Some without knowing that you’re unwrapping a Some. You almost always want to take the value out, transform it, and then put it back into a Some (or return a Nothing). Additionally, you can’t unwrap a Nothing - which is the important part of the Some/Nothing duality, and which must be handled by code expecting a Some. In haskell that is enforced by requiring you to fulfill the types - and Julia doesn’t enforce things to that degree (unfortunately).
This is at odds with having a standard “unwrap” (and here I agree with @mbauman), because the fact of having received a Some (and not some other comonad) is a meaningful distinction. It’s wrong to think of Some as just another single object container, so I think the goal of unifying the interface there (while conceptually/mathematically feasible) is questionable at best.
All of this combined - doubling the meaning of get like this seems like a very bad idea to me.
As with every method/api which you are using, you of course should think about whether it makes sense. I don’t see anything special here about extract. Almost every julia method is generic, so you should better make sure that the method makes sense for your types.
Hence I cannot follow the two of you @Sukera and @mbauman - the meaning is enough well defined so that this api is useful for people (extracting a single canonical value from a wrapper). Same as other methods which are enough well defined to be generic apis (like map, fetch, etc.).
Concretely I cannot imagine a single Julia user who does not understand what extract(Some(1)) or extract(Ref(1)) or extract(Val(1)) would be doing if they see its documentation is saying extract a single canonical value from a wrapper.
Sure, but that’s not my point. I’m saying that if you’re already expecting a Some there, why use the (generic) extract, when there’s the explicit something for unwrapping, which explicitly shouldn’t take anything other than Some? I can’t imagine a case where I want to be generic over the “wrapper type” and don’t already care about getting a Some and its specific semantic meaning. The same goes for Ref (assign to a location somewhere other than my scope) and Val (lift a value to the type domain) - the wrapper type has a semantic meaning in the context its used in, beyond being a wrapper for something else.
I should also note that the comonad page I linked above is in a haskell package, not the main distribution. So by default, Some doesn’t act like a comonad (though functionally it probably can be).
I’m a little confused (and maybe that’s part of the points being raised here) about what the unwrapping is for. The title says singleton wrappers, but it lists Ref and Some, which do not have singleton concrete types. A singleton type either has no fields or only has singleton-typed fields (which take up no memory in the instance), so whatever is being unwrapped is in the type parameters e.g. T in Val{T}. It’s different from indexing a Ref{T} or accessing the .val field of Some{T} for an instance of T.
Aside, what is Some for? I read the docs and still don’t know why you would need to distinguish a “presence of nothing” via Some(nothing) from an “absence of a value” via nothing, I’ve only ever needed the latter.
FWIW we had a similar debate recently about unwrap in DataAPI: Don't define unwrap(x::Any) · Issue #59 · JuliaData/DataAPI.jl · GitHub. The complaint is that we define a fallback unwrap(x::Any) = x, so we don’t throw an error if you accidentally call unwrap on a value which isn’t supposed to work in that context.
Also, as a historical point, when Nullable was a thing in Base, one would extract it using get(x::Nullable) (which throwed if the object was null) or get(x::Nullable, default). This made sense because of the presence of default. The current equivalent of this pattern is something(x::Union{Some, Nothing}) or something(x::Union{Some, Nothing}, default).