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

Thanks everyone for the replys, unfortunately I did not manage to proceed as this is only a side project.

I will try to clarify whats the intendet purpose of the macro:
For a larger algorithm, there can be different types of materials deployed. It basicly is always a function of some arbitrary parameters and a strain vector, sometimes some other values but that should not be a concern atm.

So basicly, the macro is there to deploy a material subroutine to the solving process, which is simply called with name.stress; where name is the material name. The end-user then only needs to worry about the function strain->stress and nothing else within the code.

To make life easy, we had the idea to avoid a 2D and 3D implementation, and only go for the 3D variant and make it available for the 2D computations. So what I basicly wanted is to add a functionallity to the macro that automates the parsing process from 2D to 3D strain and back, only for the 2D computations.

I think this gave me the right hint to solve my problem. It should even solve some other issues which I had in sight down the road.

The benefit comes from another part of my code, where I demand as a type the parent struct. Creating the subroutine with a macro ensures that everything will work as the algorithm demands it. (See some remarks above).