Exactly! That’s precisely what I’m getting at with unwrapping — it’s really an implementation detail of an individual type and doesn’t belong to a generic API.
I don’t share this opinion. The generic API is defined by the common understanding, and this is “extracting the single value of a single-value wrapper”.
This is not an implementation detail (by definition for me - you can define the single-value wrapper in many different ways)
There’s nothing about “single value wrapper” that is really very generic though, at least in the sense that I’d want to use Ref
, Val
and Some
interchangeably. The semantics of those three types individually are much more important than the fact that they can all hold only a single value.
As asked above - do you have an example for something where you’d be happy to accept either of those three and only care about the value inside, but NOT the fact that you got any specific one of these?
But what is a wrapper and why do I want to remove it? Is a Diagonal
a wrapper? What about Set
? Is a Ptr{T}
a wrapper? And if so, does it wrap a UInt
or a T
? All those things “wrap” a single field/value, too.
Ptr{T}
is actually even worse, because it doesn’t necessarily hold a value of type T
- truthfully, loading from the pointer can go wrong.
In fact, Ptr{T}
doesn’t hold (or more precisely, own) the object it may or may not point to at all.
Many more builtin single-value wrappers!! Awesome. Yes this makes for a more complete picture.
If Ptr
does not have a canonical single value to be extracted, then it probably does not fit this general extract
api.
Here my motivating example (I probably should have minified this way earlier in the discussion ) :
lang_specific(::Val{:jl}) = "this is very specific code to support .jl files"
function my_generic_func(langwrapper)
# we have some helper functions which depend on a wrapper type which is not interesting to this "generic" function
lang_specific(langwrapper)
# instead of print you can imagine anything else which may depend on the actual value
lang = only(langwrapper)
print("the current language used is: $lang")
end
EDIT: while this case may count as “generic” function or not, in general the goal is really rather mental simplicity, so that I don’t need to think or search every time for the specific unwrapper (if there is one at all).
That’s indeed a good example where your proposal would help. But it’s quite the edge case…
Here’s a parodical analogy to better explain why I dislike the proposal:
I can never remember all the ways of using values in Julia, so I’ve defined the following:
# Using a function means calling it use(x::Function, y...) = x(y...) # Using a command means running it use(x::Cmd) = run(x) # Using a mutex means locking it (call `unuse` to unlock) use(x::ReentrantLock) = lock(x) # Using a regex means matching text use(x::Regex, y) = match(x, y) # Using a channel with a value means sending the value use(x::Channel, y) = put!(x, y) # Using an array means accessing the values use(x::AbstractArray, y...) = x[y...]
Of course it’s not a fair analogy (“use” is more ambiguous than unwrapping). But I think it shows in which wrong direction this proposal is going: all code is going to look like a bunch of use
. It’s hiding the semantics behind the types of the values. These types are often not obvious so the code is less readable. You basically have to run type inference in your head to make sense of it.
I really don’t know what to take from it
It sounds like a song against using common APIs. A bit like “always use the implementation detail if possible”.
I don’t think so. I am relatively often running into this - probably as often as I am using Val
, which is actually quite often. And others also reported to define their own unwrap
method every time.
I don’t have to run type-inference in my head in order to fruitfully use a function like only
.
But do you agree this is showing a downside of your proposal? I agree there’s an upside in common API, maybe we just disagree on the weights of the upside and downside.
I’m saying the example is an edge case where the proposal makes things significantly easier. I think what you often run into is a wish for extract
so you don’t have to remember which function to use? That’s quite different… And I suspect the unwrap
defined by others fall in the same category.
The problem is more when you read code written by others (or a younger self), to understand what the only
call (and the code around it) is really doing.
yes, exactly (and sometimes there even is no function, and in case of Val there even is no field)
This is great test to check whether something is really more useful than harmful. I like it a lot. Cannot say whether my later I would be confused by only
.
I guess a bit, but only so far as I am not yet used to its already established meaning. Hence, only so far as people already get confused by only
when they seeing it the first time being applied to a Vector.
That my_generic_func
example is actually perfect for expressing the troubles with this. It looks like it could support any language implementation. I could decide it’s worth adding support for Javascript with a struct that needs a parsing state machine:
struct JS
parser_state::StateMachine
JS() = new(StateMachine())
end
Now what happens when I try running my_generic_func(JS())
— and importantly, does JS
implement an unwrap/only method to access its state machine? If so, suddenly you’re telling folks about the internals of its state machine instead of the language you meant. Now you may say: OK Matt, that’s pretty contrived — just don’t implement unwrap/only for it! But let’s take it one step farther. Let’s implement a specific parser for JSON that wraps a JS parser and just limits the language in a few specific ways:
struct JSON
language::JS
end
Now here it seems useful to access the underlying language in some contexts, no? Let’s implement unwrap for it! Oh, wait, now your my_generic_func
is even more subtly wrong: it’s reporting JSON
inputs as JS
.
The answer is that your my_generic_func
isn’t generic. It’s implicitly dependent upon my_generic_func(langwrapper::Val)
and will break in unexpected ways if you pass anything else.
To put a slightly finer point on it, the problem isn’t so much that “unwrapping” isn’t well-defined, it’s that wrappers themselves aren’t well defined. You might wrap a meaningful name, but you also might wrap an internal piece of machinery, or maybe you wrap an externally-visible thing to slightly change its behavior, or maybe you wrap something entirely different.
while I don’t understand your example on the first read through, I guess you are telling, that I should rather define a getlangsymbol
unwrapper, which specifically targets my usage of the unwrapping I need. I need more time to think about it.
(Of course in my example I was rather looking for something builtin, so that I don’t need to define an extra type function - you seem to be telling, that there couldn’t be any helpful builtin… because of the concept of “wrapping” being ill-defined… need to think a bit more about it. )
It does not mean that we’re free to invent new semantics for what a function means.
Actually we are free to invent new semantics for functions. People do it frequently. I certainly believe that we ought not do it. An example is SymbolicUtils which uses ==
not to compare values (returning Bool
), but rather to construct an equation. And then extends Base.isequal
to compare two values of types in the package, when the docstring says isequal
is concerned with “treatment of floating point numbers and of missing values.”
I always trot out this example. I could find another example to avoid always pointing my finger in the same place. I would not be surprised to find a similar abuse of multiple dispatch in a package I wrote, especially an older one.
The discussion here is about adding/changing Julia’s builtin definitions, which requires clearing a much higher bar.
I have read up some comonad literature and realized that there is paradigm example called the Store Comonad (reference).
At the end it says, it is the base for the concept of a Lens, i.e. of getters and setters, just without having the fieldname which you want to set/get.
This intuition reminds me a lot of the two Julia apis
getindex(a) # a[]
setindex(a, s) # a[] = s
which are exactly supported by Ref
.
I guess indexing captures very well where my goal lies between implementation detail and having a semantically fully specified api (like in the example getlangsymbol
): It is more a generic helper to work with structure.
A wrapper of a wrapper of a wrapper of course would need nested indexing, but nevertheless people somehow agreed that indexing is a valuable concept. You can build another helper-wrapper which somehow simplifies indexing into a complex nesting of wrappers. That all fits the general idea of indexing (a kind of unstructuring if you will).
Given this thought, I am surprisingly coming to the intermediate conclusion that getindex
might be the most fitting general api for “unwrapping a single-value wrapper”. (of course get
and only
also have a relation to the concept of unstructuring, but for me it feels slightly less so)
Sure! Then the key question is if the type wants to fully behave like an indexable (and likely iterable) container of one element. And then there’s a whole host of well-documented and defined expectations and behaviors that come with that decision.
It’s not always obvious if all those semantics are what you want, though, and there are big ramifications about how the thing behaves in other parts of Julia — like broadcasting, for example.
Now I also better understand this comment of yours.
A field access is also some kind of destructuring, similar to indexing
Downsides of field access
existing single-value wrappers have different fields already … this is probably hard to unify
Downsides of getindex
indexing often comes with supporting iterable (e.g. Ref does so), but this might unwanted by certain wrappers.
I think indexing would be great, but only if it is allowed to come without supporting iterator.
@mbauman which documentation are you referring to?
Downside of a new function
With the new understanding, that this needs to make sure that people understand it as a kind of unstructuring like field access or indexing, I think get and only are suboptimal.
While unwrap
seems to go into the right direction, it can understand that might be probably confusing as a general api.
It is hard to think of another name which makes clear that this is an unstructuring process similar to getindex or field access…