How to pass a relative UnitRange to a function?

I have the following which tries to apply a relative unit range to a string whose length is unknown:

function testnums(data::String,numrange::String)
    data[numrange]
end

foo = "asdfasdfasdf"
bar = "end-3:end"
testnums(foo,bar)
Here is the error ``` MethodError: no method matching getindex(::String, ::String) Closest candidates are: getindex(::String, !Matched::UnitRange{Int64}) at strings/string.jl:245 getindex(::String, !Matched::Int64) at strings/string.jl:210 getindex(::String, !Matched::UnitRange{#s69} where #s69<:Integer) at strings/string.jl:242 ... testnums(::String, ::String) at passargs.jl:2 top-level scope at passargs.jl:7 ```

As suggested by the error, you can’t pass a string for the range. So how can I pass a relative range eg end-3:end?

1 Like

I’d just build the range inside the function.

function testnums(data::String, n)
    data[end-n:end]
end

Does that work for you?

Well,

bar is not a range it’s a string. You can change it to a range like this

function testnums(data::String,numrange::String)
    data[eval(Meta.parse(numrange))]
end

foo = "asdfasdfasdf"
bar = "end-3:end"
testnums(foo,bar)

What you should do, if you truly want to pass a relative range, is to pass a normal range, and the index it’s relative to, and add those up inside the function. For example, for end, it would be rel=length(data), and then you do rge=-3:0, and then in the function you take data[rel.+rge]. Otherwise, you need to pass the absolute range.

2 Likes

Please don’t do this. This is a pathological example of pretty much every “gotcha” in the julia language put together. It relies both on using strings as expressions (using parse) and on global variables and using eval. There are so many subtleties to just how scary this is that it would even be difficult adequately explaining it :laughing:. Luckily, it throws a ParseError.

The short answer to this is that since end is a keyword (not a function or a symbol), you can’t pass it around. In particular notice what happens when the expression a[1:end] is “lowered” (translated by julia into what’s actually going to get compiled).

julia> Meta.@lower a[1:end]
:($(Expr(:thunk, CodeInfo(
    @ none within `top-level scope'
1 ─ %1 = Base.lastindex(a)
│   %2 = 1:%1
│   %3 = Base.getindex(a, %2)
└──      return %3
))))

In particular, note that %1 is evaluating lastindex(a), %2 constructs the range 1:%1 (i.e. 1:lastindex(a)), and then that’s what’s used as the index in %3. The keyword end is just syntactic sugar for lastindex(self), so it doesn’t play a direct role in any of this and julia gets rid of it long before compile time. To do this you can:

  1. pass in the index ranges you want directly,
  2. or you can write a macro (I advise against, but it is technically the “best” solution)
  3. or you create an api that doesn’t require it. If your example is the behavior that’s important to you, you could instead do
function testnums(data, back_i)
    data[end-back_i:end]
end

foo = "123456789"
testnums(foo, 3) # "6789"
4 Likes

Ha yes, I just (tried) to answer his question. But it is true that this is not best practice. I just assumed that he has a good reason to pass strings to the function.

What would work though is

function testnums(data::String,numrange::String)
           exp = reduce(*, ["data[",numrange,"]"])
           eval(Meta.parse(exp))
           end

foo = "asdfasdfasdf"
bar = "end-3:end"
testnums(foo,bar)

But it is certainly not the way to go. If you don’t have a specific reason to use a String argument for the range then dont.

I have faced the same issue of not being able to pass relative ranges to functions. Coming from the python world where we could write the same function as,

def testnums(data, numrange):
    return data[numrange]

foo = "asdfasdfasdf"
bar = slice(-3,None)
testnums(foo,bar) # Output: 'sdf'

I understand why the code provided by the OP doesn’t work. But shouldn’t there be a counterpart to python’s slice in Julia which allows easily passing relative ranges to functions. Would something like RelativeRange(end-3,end) be hard to implement in Julia? Or is there something fundamentally flawed about passing relative ranges as arguments to functions?

One could do something like this (not sure how useful it is)

struct Slice
    a::Union{Int, Nothing}
    b::Union{Int, Nothing}
end

Slice(; a = nothing, b = nothing) = Slice(a, b)

function Base.getindex(v::AbstractVector, s::Slice)
    return if isnothing(s.a) && isnothing(s.b)
        copy(v)
    elseif isnothing(s.b)
        v[end + s.a : end]
    elseif isnothing(s.a)
        v[begin:begin + s.b]
    else
        v[s.a:s.b]
    end
end

and it works in the following way

julia> v = collect(1:10)

julia> v[Slice(a = -2)]
3-element Array{Int64,1}:
  8
  9
 10
3 Likes

It’s not possible to pass the keyword end around. I was going to say this can be done with a macro, but turns out it really can’t :disappointed_relieved:. You cannot represent a standalone end as an expression without jumping through hoops.

julia> :(a[end-3:end]) ### legal
:(a[end - 3:end])

julia> :(end-3:end) ### illegal
ERROR: syntax: unexpected "end"
Stacktrace:
 [1] top-level scope at none:1

This means you cannot do what was my first though, which is writing a macro to convert

@slicelike end-3:end

to a new type like @Skoffer’s Slice

SliceLike(x -> lastindex(x)-3, lastindex)  
# getindex with this converts to 
# x[lastindex(x)-3:lastindex(x)], 
# which is the same as with end.

or even simply to a “slicing” function like

# called like slicer(x)
slicer = x -> x[end-3:end] 

But anyway it turns out you truly can’t do this with a macro, because end is even more of a reserved keyword than I thought! I’m not sure if this is necessarily the case for some intrinsic reason, or whether :(end-3) could technically be an allowed expression, but oh well.
In any case, a custom type like Slice is still very much on the table, as is using a slicer function without a macro.

2 Likes

Thanks for the thorough answer.
Lol, this is almost exactly how I did it while waiting on a more ‘elegant’ solution from Discourse.
I was hoping that a metaprogramming technique was possible so that I could write a function that ‘understands’ relative and explicit ranges, looks like the quick and dirty solution is the only one in this case.