Conversion and promotion only automatically occur in some methods or syntax, and the method dispatched by sin(::AA) does not do that, in fact it does not exist. Technically it’s not inherently automatic either because it’s implemented somewhere, which lets you implement your own methods with automatic conversions or promotions. Off the top of my head, conversions are attempted automatically in base Julia when assigning to a type-annotated variable or field, returning from a method with an annotated return type, and pushing into an array.
Thank you @jling and @Benny , your answers and a reread of the manual suggest to me that Julia will not implicitly call convert in order to find a matching method. My example above was just an MWE to help me understand convert, what I really want to do is something like:
struct MyType <: Function end
(::MyType)(x) = println("Hello $x")
Base.convert(::Type{MyType}, x::Function) = MyType()
dosomething(x::MyType, y) = println(x(y), " says hello")
dosomething(8) do x
x^2
end
which I now can see does not work because the new convert method is not implicitly called. The main goal here is to be able to use do syntax without changing (or adding methods to) dosomething. Is there something akin to convert that can be used to achieve this?
That’s impossible. Every call is dispatched to a most fitting method, and if that method doesn’t exist or doesn’t properly implement the conversion you need, then you’re just stuck with manual converts in serial calls, like dosomething(convert(MyType, x->x^2), 8). do syntax in particular needs _(::Function, _) or _(::Any, _) methods.
If you can add a method, this can be done with a forwarding method, like:
though I’d prefer to separate responsibilities. Unlike most types, Function-annotated arguments are not specialized over as a heuristic, so you’ll want to make a parametric method to opt into specialization, search the performance tips for examples.
P.S. (::MyType)(x) currently returns nothing, so println(x(y), " says hello") will print nothing says hello. My gut feeling is that’s not your intention.
Thank you for your insights, at least it is useful to know that there is no immediately obvious slick way of handling this. I will just have to either accept that straight forward do syntax is not possible or redesign the framework. I did think of a little hack that allows me to use do syntax although less elegantly:
enable_do_syntax(f1::Function, f2::Function, args...; kwargs...) = f2(convert(MyType, f1), args...; kwargs...)
enable_do_syntax(dosomething, 8) do x
x^2
end
Yes, this was a very shaved down MWE, I did realise this, but decided that as a whole it illustrated what I was trying to achieve, namely to use do syntax on a specialised type.
seems a little elaborate, and the name suggests something too general for such a specific conversion. If you’re not trying to call dosomething directly (because you also can’t add the obviously missing method), then just make a do-supporting derivative function forwarding to it:
but I suppose the implication is that there are more functions like dosomething with missing _(::Function, _) methods, hence a higher-order function to handle any of them. A more specific name would be in order, but honestly makeMyType(dosomething, 8) do x isn’t really preferable to a manual convert prior to a typical call to me.
The do-less examples assigned to a global variable that’ll persist, but a let block is a simple alternative:
let sq = x->x^2
dosomething2(sq, 8)
end
in fact if you allow yourself to annotate MyType on the assignment, it converts and you can just use the original function:
let sq::MyType = x->x^2
dosomething(sq, 8)
end
clarity which I would prefer in the case where MyType isn’t actually special and there’s a whole host of types to convert functions to.
Thank you both @nsajko and @Benny for your comments.
We clearly disagree on whether do syntax should be part of Julia, I feel that it looks very neat, but I will consider your suggestion, especially if do is handicapped with a call to enable_do_syntax.
Hopefully I can come up with a better name.
Exactly, there are many dosomethings and they will be spread out over multiple packages. Initially I wanted to keep this question simple, but now that we are in so deep, I feel that I should mention that there are many MyTypes as well, all with one abstract supertype which is what the dosomethings actually have as their first parameter.
For a multi-line anonymous function, I’m understanding that you are saying that you would prefer syntax like:
dosomething(convert(MyType, x -> begin
println("starting...")
for vv in some_global_variable
if vv > length(vv)
fsad....
fdsa...
elseif dsfdsa....
fsdaf...
fafad...
else
error("dsfasdfada")
end
end
end), 8)
or for the let version:
let sq = x -> begin
println("starting...")
for vv in some_global_variable
if vv > length(vv)
fsad....
fdsa...
elseif dsfdsa....
fsdaf...
fafad...
else
error("dsfasdfada")
end
end
dosomething(convert(MyType, sq), 8)
end
It is especially in situations like this where I feel that do syntax looks neater which is why I want to preserve the option to use it.
I wasn’t talking about the syntax but specifying the type to convert the function to. You have confirmed that there are many such types, and manual specification makes far more sense than higher order boilerplate functions inlining the types e.g. dosomething1, dosomething2, etc.
It is conceivable for a macro to transform the neater do syntax to the code converting the input function prior to the higher-order call. I imagine:
Thank you, I have tried it on my own code and it has worked on everything except when keyword arguments are specified with ;, e.g.:
dosomething(x::MyType, args...; kwargs...) = println(args..., kwargs)
@convertdo MyType dosomething([1 2;3 4], "EE"; hi="hello") do x
x^2
end
which produces an “invalid syntax” error. I don’t know enough about writing macros to find the bug, hopefully you or someone else can see what needs to be done.
Thanks for the feedback, this is the sort of stuff that would be caught in a good test. From what I can tell, it’s because I just insert the converted function expression to the 2nd element, shifting the previous 2nd and following elements back, for the 1st argument of a call without ;. For a call with ;, the 2nd element instead holds the post-; arguments, and shifting into the 3rd element turns them into a mangled 2nd argument (see output of @macroexpand). I can make a check that gets rid of the mangling, hopefully that covers any calls because I don’t have the code to run tests:
julia> macro convertdo(type::Symbol, doblock::Expr)
if doblock.head != :do throw(ArgumentError("@convertdo's 2nd argument must be do-block.")) end
callexpr, anonfunc = doblock.args[1], doblock.args[2]
arg1 = if (callexpr.args[2] isa Expr && callexpr.args[2].head == :parameters) 3 else 2 end
insert!(callexpr.args, arg1, :(convert($type, $anonfunc)) )
callexpr
end
@convertdo (macro with 1 method)
Spotted a rookie macro mistake in my code. I only bothered to experiment a little within the Main module, so it doesn’t cover macro hygiene at all; basically, that’s whether the variables in the expression are considered to belong to the macro definition’s module or the macro call’s module. As it is, all the variables will be considered to belong to the macro definition module, but in this context, you likely want the code you write to belong to whereever you write it, and the only new symbol the macro introduces, convert, should belong to Base unconditionally. So, I’m putting the amended macro here, it’s a very small change:
julia> macro convertdo(type::Symbol, doblock::Expr)
if doblock.head != :do throw(ArgumentError("@convertdo's 2nd argument must be do-block.")) end
callexpr, anonfunc = doblock.args[1], doblock.args[2]
arg1 = if (callexpr.args[2] isa Expr && callexpr.args[2].head == :parameters) 3 else 2 end
insert!(callexpr.args, arg1, :(Base.convert($type, $anonfunc)) )
esc(callexpr) # entire expression belongs to macro call module
end
@convertdo (macro with 1 method)
julia> @macroexpand @convertdo MyType dosomething([1 2;3 4], "EE"; hi="hello") do x
x^2
end
:(dosomething(Base.convert(MyType, ((x,)->begin
#= REPL[4]:2 =#
x ^ 2
end)), [1 2; 3 4], "EE"; hi = "hello"))
See how all the Main._ from the first version’s @macroexpand vanishes, and the do-block’s argument stays as written instead of being changed? Those changes were the variables being resolved in the macro definition scope by default. If you ever need to use the macro outside its definition module, I expect this would fix the immediate bugs.