-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
syntax to replace f(::typeof(+))
?
#32541
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
::typeof(x)
?f(::typeof(+))
?
Modified the title because I originally thought this was about having a shorthand for writing tmp = <expr>
x::typeof(tmp) E.g. one could have |
On the original subject, one of the things that people have often wanted is the ability to write: fib(0) = 1
fib(1) = 1
fib(n) = fib(n-1) + fib(n-2) For that we could special case literals as arguments. Of course that doesn't help this situation since |
The two example with |
Regarding 2, @JeffBezanson are you proposing adding the ability to do value dispatch (i.e. a limited form of predicate dispatch on being |
I wrote:
So I'm not advocating adding it now, just pointing out that it would be a compatible future extension.
Yes, it is value dispatch.
Correct; |
random idea: discarding the rhs to mean whoami(+::_) = "I am plus" |
So the rule would be |
How about |
The syntax |
@StefanKarpinski: what does |
Same as |
I dislike the idea of adding new syntax just for working around the (moderate) ugliness of If new syntax is introduced, I would argue that it should enable proper value dispatch. I like @tkf's idea of using f($5) = "five" # specialize on literal value
const x = 6
f($(x)) = "six" # specialize on value of variable
f($(+)) = "plus" # equivalent to f(::typeof(+))
f($x) = x # always dispatch on value of x, equivalent to f(::Val{x}) = x, but nicer Though admittedly I have no idea whether this syntax is still available 😄 |
Using prefix |
The julia> f(::Val{3}) = "got it"
f (generic function with 1 method)
julia> f(3)
ERROR: MethodError: no method matching f(::Int64)
Closest candidates are:
f(::Val{3}) at REPL[2]:1
Stacktrace:
[1] top-level scope at REPL[3]:1
julia> f(Val(3))
"got it" Actual value dispatch, if it were implemented, would allow you to write something like this: julia> f(===3) = "got it"
f (generic function with 1 method)
julia> f(3)
"got it" Let's consider the concept of more general value dispatch (and as we'll see, this leads us somewhat inevitably to predicate dispatch) more seriously, to see where this road might lead us, which reflects back on a potential design. The nice thing about using f(x === 3) = "got it" means "if fib(n ∈ (0, 1)) = 1
fib(n) = fib(n-1) + fib(n-2) But what if you also want to constrain the type? Them maybe you can write this: fib(n::Int ∈ (0, 1)) = 1
fib(n::Int) = fib(n-1) + fib(n-2) Does that fib(0) = 1
fib(1) = 1
fib(n) = fib(n-1) + fib(n-2) However, if we were to consider fib(0::Integer) = 1
fib(1::Integer) = 1
fib(n::Integer) = fib(n-1) + fib(n-2) However, at that point, it's either shorthand for predicate dispatch on If one were going to consider predicates, putting them in the argument position along with potential type information is inevitably going to get a bit crowded. But we also have the fib(n::Integer) where {n == 0} = 1
fib(n::Integer) where {n == 1} = 1
fib(n::Integer) = fib(n-1) + fib(n-2) The fib(n::Integer) where {n ∈ (0, 1)} = 1
fib(n::Integer) = fib(n-1) + fib(n-2) There's one very significant problem with the whole predicate thing right off the bat: you need to check the predicates in some order and unless the predicates are mutually exclusive, that order matters. This would be a deviation from the current language design where (except for one issue that we'd like to fix in 2.0), method definition order does not matter. Of course, we can just say that methods are checked in order of (specificity, reverse definition) and the first one that matches is called. In other words, sort methods by specificity first, then for methods with the same specificity by reverse definition order, so most recent first, and call the first one that matches. Why reverse order of definition? Because if you define a newer method that complete occludes and older method with the same specificity, the compiler might not be able to prove that, but you still want the newer one to replace the older one. It's still hanging around in the method table, but it will never be called. Anyway, having gamed out a bit more of that, I think we should not add any form of value dispatch.
As a consequence of the compiler not being able to reason about predicates, you may, through interactive use end up with uncallable methods littering your method tables, gumming up the works. So in short, I feel like adding any form of value dispatch that isn't explicitly expressed in terms of types leads us down a dangerous path towards general predicate dispatch, which has major problems. |
I don't buy the slippery slope argument. For example, in Defining things like I like the syntax idea |
There's is a way out of this difficulty: by lowering directly to something we already have (much as how ComputedFieldTypes.jl already implements #18466 or that keyword args are syntax for doing The resulting function would have the aforementioned issue with order dependence to resolve ambiguities. But ignoring that for now, at some point we could turn this:
into this:
(we could have a macro for simulating this, or perhaps Match.jl essentially already has a macro for this) |
I normally don't buy slippery slope arguments, but this is a very specific very slipper rock: i.e. that
This is very clearly in the type domain though, not in the value domain. It supports values that can be type parameters—no more, no less—which is simple to explain and easy to understand. Once you start expressing this in the value domain, I think that it will be much harder to understand why only value judgements that can be translated into the type domain are allowed.
Well, that's good :). I think it has the benefit that the predicate occurs in a place where people are used to only type-oriented things going, which makes it a bit easier to explain why only |
The idea of requiring putting all the methods in a single block is interesting. |
I'm trying to understand the limitations of the existing value type system. The manual says that the type parameter of f(::Val{sin}) = 1
f(::Val{+}) = 2
f(::typeof(sin)) = 3
f(::typeof(+)) = 4
@assert f(Val{sin}()) == 1
@assert f(sin) == 3
... All of these work as expected, which surprises me, since functions are presumably not bits types. Are they being interpreted as symbols when used as a type parameter? Also, if I try to pass a lambda as type parameter, the method is created just fine, but cannot be called: f(::Val{x->2x}) = 5
f(::typeof(x->2x}) = 6
f(Val{x->2x}()) # error
f(x->2x) # error Could someone explain these behaviors? And also whether there is any difference between dispatching on |
Typical functions all have their own types. Each anonymous function has a type generated for it:
so they're different.
|
Thanks for the explanation! |
Also note that |
What do you mean by this? I'm getting f(::typeof(+)) = 1
g = +
@assert g === +
@assert f(g) == 1 Same as with Edit: Never mind. That's just another instance of functions being singletons. |
f(x::StaticVector{n}, y::StaticVector{m}) where {n === 2m} = ... would work. But I guess that's essentially #18466. |
Interesting clarification @ #32541 (comment) In fact @test isbitstype(Val{T} where T) == false But becomes one when is arg is specified (even with a non bits param like a symbol) @test sizeof(Val{:a}) == 0 # has no data
@test Val{:a}() == Val{:a}() # is singleton
@test Base.unwrap_unionall(Val).mutable == false # is immutable
@test isbitstype(Val{:a})
@test isbits(:a) == false # param is not bits So the compiler could statically optimize dispatch |
|
I was guessing it was due to something like that. I dont want to hijack this topic, but testing val with the predicates of reflection.jl v1.3.0-rc4 using Printf
foreach([
Base.isimmutable,
Base.isstructtype,
Base.isprimitivetype,
Base.isbitstype,
Base.isbits,
Base.isdispatchtuple,
Base.iskindtype,
Base.isconcretedispatch,
Base.isdispatchelem,
Base.isconcretetype,
Base.isabstracttype,
Base.issingletontype,
]) do f
@printf("%-30s => %c\n", "$f(Val)", (f(Val) ? 'x' : '-'))
end produces
So i don't get why -- foreach([
Base.isimmutable,
Base.isstructtype,
Base.isprimitivetype,
Base.isbitstype,
Base.isbits,
Base.isdispatchtuple,
Base.iskindtype,
Base.isconcretedispatch,
Base.isdispatchelem,
Base.isconcretetype,
Base.isabstracttype,
Base.issingletontype,
]) do f
@printf("%-30s => %c\n", "$f(Val{:a})", (f(Val{:a}) ? 'x' : '-'))
end
|
Please continue at the discourse thread you've already started.
The word "abstract type" is ambiguous, much like bitstype did before one of the meanings was renamed. That's why in the other thread I said it's not a concrete type instead. |
It's not really ambiguous: a type |
Extending the single block idea, how about these: #here the advantage is that we have a guaranteed common signature
piecewise fib(n::T)::T where {T<:Integer}
fib(0) = 1
fib(1) = 1
fib(n) = fib(n-1) + fib(n-2)
end By scanning for literals (or even unbound values) this could be "easily" transformed into: function fib(n::T)::T where {T<:Integer}
n===0 && return 1
n===1 && return 1
n===n && return fib(n-1) + fib(n-2) # n===n cannot fail
end The bad thing though is, that in more argument cases the order of the statements may be important except if we will do full ambiguity parsing (which might not be possible in the general case, I guess). Optimization potential: #inline all
var"literal_fib"(::typeof(fib), ::Val{0}) = 1
var"literal_fib"(::typeof(fib), ::Val{1}) = 1
#catch-all, similar to n===n
var"literal_fib"(f::typeof(fib), n) = f(valextractor(n)) where we need those @inline valextractor(x::Val{v}) where v = v
@inline valextractor(x) = x and doing the corresponding replacements By that proposal, the piecewise ^(x::T, p::T)::T where {T<:Integer}
const ^(x, -1) = inv(x) #marked for literal-only
const ^(x, 0) = one(x)
const ^(x, 1) = copy(x)
const ^(x, 2) = x*x
^(x, p) = power_by_squaring(x,p)
end which would turn into: function ^(x::T, p::T)::T where {T<:Integer}
#(x,p)===(x, -1) && inv(x) #as we marked this branch it won't make it into the runtime variant
#(x,p)===(x, 0) && return one(x)
#(x,p)===(x, 1) && return copy(x)
#(x,p)===(x, 2) && return x*x
(x,p)===(x, p) && return power_by_squaring(x,p) #this === cannot fail
end of course, if we want more runtime optimizations we can remove the #inline all
var"literal_^"(::typeof(^), x, ::Val{-1}) where {x,p} = inv(x)
var"literal_^"(::typeof(^), x, ::Val{0}) where {x,p} = one(x)
var"literal_^"(::typeof(^), x, ::Val{1}) where {x,p} = copy(x)
var"literal_^"(::typeof(^), x, ::Val{2}) where {x,p} = x*x
#catch-all, till here are branches where === can fail
var"literal_^"(f::typeof(^), x, p) = f(valextractor.(x,p)...) OT:
As you state clearly how to understand abstract, where is my mistake when I think that these don't match: |
The original problem would resolve like that: piecewise f(binaryop, a, b)
f(+, a, b) = a+b
f(-, a, b) = a-b
f(mapreduce, a, b) = mapreduce(a,+,b) #sure, this is arbitrary
f(binaryop, a, b) = binaryop(a,b)
end Though, I now see, we need some way to ensure that the case that nothing matched is handled. *see Edit piecewise f(+, a, b)
return add(a,b)
end This wouldn't be problematic as we can distinguish depending on the structure of the body (if there are "top-level" definitions). If we have such a single line case, then we would have two possible solutions:
function f(_1_::typeof(+), a, b)
_1_===+ && return a+b #if we use the first solution we may even omit the check, as it is a singleton
throw(...)
end EDIT: |
This is a pretty common and useful idiom that I believe has two problems:
f(::typeof(+))
is to select for the callf(+)
, but it only works if+
is a singleton. E.g.f(::typeof([1]))
is just a strange way to writef(::Vector{Int})
, and will clearly not select onlyf([1])
.Some syntax would fix this. The two ideas that come to mind are
or
An argument name could be specified too, e.g.
g(f === +) = ...
. We could also enable unary parsing of those operators, allowingf(===dot) = ...
.That would mean the method should only be called if the argument is
===
to the specified value. Of course, we currently only support that for singleton objects, allowing us to give an error if it's not possible to dispatch on the object you want, instead of silently defining a method for a wider type. In the future, we'd have the option of allowing dispatch on more kinds of values, but that of course is a separate issue.The text was updated successfully, but these errors were encountered: