Macro usage: create dispatch for vector length

Hello,

I have an Idea I would like to implement for a module I am working on. I will try to explain, but I am not sure if such implementation would make much sense, probably there are better solutions.

I have a macro, that looks somewhat like the following:

abstract type SomeThing end
macro some_macro(name, list, func)
   str = :( 
         struct $name <: SomeThing
            list::Tuple
            func::Function

            function $name(list::Tuple)
               return new(list, $func)
         end
      end
      )
   return str
end

The module user should then be able to add their own struct, containing a user defined method which is then called in another part of the code. Using the macro shall ensure, that the structure that is needed is contained.

It is then created with

@some_macro Name (:a, :b) function (vec)
   # Do something with the parameters in the list and vec -> result
   return # result
end

However, the input vector can be of length 3 or 6. Right now, the user has to implement a routine for each case, but I would like to have it simplified that only one is necessary. For this, I would like to have an additional dispatch included in the macro. It should do something like this:

if length(vec) == 3
   # transform to length 6 with zeros
   new_vec[1] = vec[1]
   #...
   new_vec[3] = 0
   #...
   new_vec[6] = 0
else
   # keep lengt,
end 

However, the routine where the struct is used calls the function defined in the second codeblock like this:

Struc1.Struc2.List[i].func(vec)

I hope it is understandable what I want to achive. If so, how would I go for this?
I think I pretty much want to append the if/else in front of the users function. But How would I dismantle the function to do so?

I’m not entirely clear on your desires, but I’m taking my best guess here.

If you aren’t already using StaticArrays.jl at these vector sizes, you should strongly consider it.

As far as your promote-3-vectors-to-6-vectors desire (as I understand it, which may be wrong)

using StaticArrays

padto6(x::SVector{3}) = vcat(x, zero(x)) # pad with zeros to length 6
padto6(x::SVector{6}) = x # do nothing
padto6(x::AbstractVector) = length(x)==3 ? padto6(SVector{3}(x)) : SVector{6}(x) # error on vectors with length != 6

foo(x::AbstractVector) = foo(padto6(x)) # turn into SVector{6} and call again
function foo(x::SVector{6})
  # do stuff
end

You could write a macro to autodefine the conversion method when you define foo, but maybe that isn’t worth the effort.

You should consider specializing $name on the type of the input list with {} for performance. Also, if you’re only using one function for the entire type, just make a function to dispatch on it rather than store it in the struct.

struct name{T<:Tuple} <: SomeThing
	list::T
end
function dostuff(x::name, vec)
	# do stuff with x.list and vec
end

# instead of Struc1.Struc2.List[i].func(vec)
# now use dostuff(Struc1.Struc2.List[i], vec)

EDIT: or, as pointed out below, you may not need a different types for this. In which case you should include the func field and specialize on it:

struct name{T<:Tuple, F} <: SomeThing
	list::T
	func::F
end

You can even make the struct “callable” with (whether you have x.func as a field or not) with

function (x::name)(vec)
    # do stuff with x.list, vec, and x.func (if you have it)
    # this might just be to `return x.func(x.list, vec)`
end

# now use Struc1.Struc2.List[i](vec)

I don’t understand why’d you want to create new structs in this manner. They only differ in name, so why not just use a single struct (name)? Is it just to give a default value for func? Could you give more info about how you envision the user using your macro?

Also, as far as I can tell your list parameter in the macro is not used?

1 Like

I’m not sure this is the behaviour heronrick wants, but you don’t even need func here:

struct MyStruct{T<:Tuple, F} <: SomeThing
    list::T
end

function dostuff(ms::MyStruct{T, F}) where {T, F}
    return F(ms.list)
end

ms_sum = MyStruct{Tuple, sum}((1, 2))
dostuff(ms_sum)  # returns 3 (= 1 + 2)

ms_custom = MyStruct{Tuple, t -> t[1] + 2t[2]}((1, 2))
dostuff(ms_custom)  # returns 5 (= 1 + 2*2)

Of course, you could also just call sum((1, 2)) etc. directly :slight_smile:, so the benefit of the struct is still lost on me.

EDIT: I suppose if you now use
MyStructSum{T <: Tuple} = MyStruct{T, sum}
for example, you get something very similar to the original macro output*, without the need for the macro.

mss = MyStructSum{Tuple}((1, 2))
dostuff(mss)  # 3

*:

@some_macro MyStructSumViaMacro (:a, :b) sum  # (the tuple is irrelevant)
mssvm = MyStructSumViaMacro((1, 2))
dump(mssvm)
#=
MyStructSumViaMacro
  list: Tuple{Int64, Int64}
    1: Int64 1
    2: Int64 2
  func: sum (function of type typeof(sum))
=#

You need to store the function if it’s not definitely always a isbitstype. For example, if the function closes over a Vector then you cannot call it from (or even make it) a type parameter.

julia> struct func{F} end

julia> func(f) = func{f}()
func

julia> func(sum)
func{sum}()

julia> func(Base.Fix1(*, [1;2;3]'))
ERROR: TypeError: in Type, in parameter, expected Type, got a value of type Base.Fix1{typeof(*), LinearAlgebra.Adjoint{Int64, Vector{Int64}}}

julia> Base.Fix1(*, [1;2;3]') isa Function
true
1 Like

Interestingly, anonymous functions seem to work

julia> fc = func(x -> x * [1 2 3])
func{var"#1#2"()}()

julia> evaluate(::func{F}, t) where F = F(t)
evaluate (generic function with 1 method)

julia> evaluate(fc, [1; 2; 3])
3Ă—3 Matrix{Int64}:
 1  2  3
 2  4  6
 3  6  9

julia> anon = x -> x * [1 2 3];

julia> isbitstype(anon)
false

julia> anon isa Function
true

In any case, a good remark! Using a field func::F will then be the safer option.

Anonymous functions often work in this situation, though not always. When an anonymous function might be instantiated multiple times with different captures, it will basically form a callable struct and run into the same problem:

# note the anonymous functions are all the same type but capture a SubArray
julia> [x -> x * z for z in eachrow(randn(3,3))] 
3-element Vector{var"#24#26"{SubArray{Float64, 1, Matrix{Float64}, Tuple{Int64, Base.Slice{Base.OneTo{Int64}}}, true}}}:
 #24 (generic function with 1 method)
 #24 (generic function with 1 method)
 #24 (generic function with 1 method)

julia> [func(x -> x * z) for z in eachrow(randn(3,3))]
ERROR: TypeError: in Type, in parameter, expected Type, got a value of type var"#28#30"{SubArray{Float64, 1, Matrix{Float64}, Tuple{Int64, Base.Slice{Base.OneTo{Int64}}}, true}}

But to your larger point, always making the function a field is the safest approach. A function’s size is usually just the size of its captures, so singleton functions like sum will occupy 0 bytes within a struct (when concretely typed) so there is no memory/performance penalty to this.

1 Like