Unifying unwrapping single-value wrappers

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)
  • Val doesn’t have any unwrap method - there is even an issue for it API request: a function to unwrap Val · Issue #34758 · JuliaLang/julia · GitHub
  • 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).

1 Like

We have something that unwraps a Some. Although it is more general:

Return the first value in the arguments which is not equal to nothing, if any.

1 Like

We could make get default to doing a getindex with no args. We can even do the reverse too although I don’t love having circular defaults.

2 Likes

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.

2 Likes

There’s a somewhat parallel discussion here: Single-argument `getindex` only works for some types · Issue #51712 · JuliaLang/julia · GitHub

1 Like

After quite a bit of searching, I think what you’re describing is that you want a consistent function to make things a comonad: Control.Comonad

Which would be extract (and it’s wrapping counterpart, extend).

4 Likes

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.

5 Likes

Indeed! For example, should the proposed function also unwrap array wrappers, ie do parent(x) for abstractarrays?

1 Like

At the moment, unfortunately, something(x::Any) = x. It really should error, though.

2 Likes

the answer was already given by @Sukera

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.

1 Like

While julia does not enforce the types no one stops you from using comonad methods in a safe way.

I agree that the combination of Some/Nothing is not very appropriate for monadic programming, please check out my packages GitHub - JuliaFunctional/DataTypesBasic.jl: Option, Try, Either, and some more common basic DataTypes (and GitHub - JuliaFunctional/TypeClasses.jl: Monoid, Functor, Applicative, Monad and more) which fill this need.

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).

While we’re talking about this – WeakRef doesn’t have a function to unwrap. The docs just use the internal property (.val if I remember correctly).

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.

Personally I just define _unwrap manually in every single package :upside_down_face:

@Benny that’s a good point. I’ve argued previously that unwrap on Val is not the same as unwrap on Ref.

What we have is two separate requirements, both that currently lack consistent syntax:

  1. unwrapping instantiated objects with a single field (Ref case)
  2. unwrapping types with a single parameter. (Val{T} case)

We shouldn’t mix them.

  1. should return the value of Ref like = ref[], and not have methods for Val
  2. should return T in Val{T}, and could be argued to return the type of Ref like T in Ref{T}

unwrap and unwraptype, or extract and extracttype could work.

1 Like

When you don’t distinguish these two cases, the result of some functions becomes ambiguous: see `findfirst` on dictionaries returns ambiguous result · Issue #29565 · JuliaLang/julia · GitHub for an example where you cannot tell whether the element was or was not found in the dictionary.

3 Likes

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).

2 Likes