Defining custom brackets for custom objects

Is it possible to define a method/function/macro/etc so that I can use ⟦⟧ to index a custom vector/struct? I’m looking for some kind of trick that would allow me to do something like the following:

struct MyVec
    data
    block_size
end

v = MyVec([1 2 3 4 5 6 7 8 9 10], 2)

...what I need...

i = 1
v⟦i⟧ = [5 5] # == v[(i-1)*v.block_size+1:i*v.block_size] = [5 5]

v.data == [5 5 3 4 5 6 7 8 9 10]
>> true

Are you asking because you also need regular indexing to work alongside this? If not, you can simply overload Base.getindex and Base.setindex! to define whatever indexing behavior you want for your struct.
Using custom symbols as brackets isn’t possible though, and it turns out that those particular symbols can’t be used at all…

julia> ⟧ = 1
ERROR: syntax: invalid character "⟧" near column 1

Exactly.

I see. I got suspicious because at https://github.com/JuliaLang/julia/issues/8934 one user replied: “I’d prefer to leave them free for packages to use.”

What if I use {} instead of custom symbols? Would that be possible?

Yeah, this can be done with a macro if you really want:

using MacroTools: postwalk, @capture

macro foo(expr)
    (esc ∘ postwalk)(expr) do ex
        if @capture(ex, v_{i_})
            ex = :($v.data[($i-1)*$v.block_size+1:$i*$v.block_size])
        end
        ex
    end
end


struct MyVec
    data
    block_size
end

v = MyVec([1 2 3 4 5 6 7 8 9 10], 2)

i = 1

Now at the repl:

julia> @foo v{i} = [5 5]
1×2 Array{Int64,2}:
 5  5

julia> v
MyVec([5 5 … 9 10], 2)

If you wanna use delimiters like , you’d have to use a string macro to pre-process before building up an AST and manipulating it.

2 Likes

Amazing!

Just out of curiosity, could it be done without macros?

Here’s what you can do. Make your custom vector callable. Then you effectively have two different ways to index your vector:

v[1]
v(1)
1 Like

No, {} is not a valid indexing delimiter in julia, but it parses like one specifically so they can be used in macros. We can lessen the amount of logic that’s in the macro though and offload the work to a special mygetindex function:

using MacroTools: prewalk, @capture
macro foo(expr)
    (esc ∘ prewalk)(expr) do ex
        if @capture(ex, (v_{i__} = x_))
            :($mysetindex!($v, $x, $(i...)))
        else
            ex
        end
    end
end

mysetindex!(v::MyVec, x, i) = v.data[(i-1)*v.block_size+1:i*v.block_size] = x
julia> @foo begin
           v = MyVec([1 2 3 4 5 6 7 8 9 10], 2)
           i = 1
           v{i} = [5 5]
           v.data
       end
1×10 Array{Int64,2}:
 5  5  3  4  5  6  7  8  9  10
1 Like

Here’s an example of the sort of pre-processing you can do in a string macro and then do regular AST manipulation:

macro foo_str(s)
    s′ = foldl(replace, ('⟦' => '{', '⟧' => '}'), init=s)
    ex = esc(Meta.parse(s′))
    :(@foo $ex)
end
julia> foo"""
       begin
           v = MyVec([1 2 3 4 5 6 7 8 9 10], 2)
           i = 1
           v⟦i⟧ = [5 5]
           v.data
       end
       """
1×10 Array{Int64,2}:
 5  5  3  4  5  6  7  8  9  10
1 Like

Thanks everyone. All these answers are very useful.