Implementing module, MethodError: objects of type Module are not callable

I am trying to implement an R-like matrix where the columns/rows can be referred to by named indices.

using Revise
using Base

include("nm.jl")
using .NM

nm = NM.NamedMatrix(zeros(3, 2), ["row1", "row2", "row3"], ["col1", "col2"])

nm[["row1"], ["col1"]]

where nm.jl looks like

module NM

import Base

export NamedMatrix, getindex

mutable struct NamedMatrix
    matrix::Array{Float64,2}
    colnames::Vector{String}
    rownames::Vector{String}
end

function Base.getindex(x::NamedMatrix, i, j)
    if !(i == Base.(:))
        i = indexin(i, x.rownames)
    end
    if !(j == Base.(:))
        j = indexin(j, x.colnames)
    end
    NamedMatrix(x.matrix[i, j],
        x.rownames[i],        
        x.colnames[j])
end

end # module

However,

julia> nm[["row1"], ["col1"]]

gives this error:

MethodError: objects of type Module are not callable

I am not sure why this is since I don’t recognize any naming conflicts with the Module name.

I’ll go through some of the mistakes:

This isn’t necessary, don’t do it.

Don’t export getindex, it’s not necessary and doesn’t do what you think it does.

The mutable is almost surely not necessary (depending on what you want to do with the type).

The order of colnames and rownames isn’t consistent with the order you assume when you call the constructor. Swap them, for example.

This is the reason for the “objects of type Module are not callable” error. Just do : instead.

Just do x.matrix[i, j], not sure what were you aiming for here.

Lastly, for performance reasons you probably want to keep the type of the variables i and j constant.

3 Likes

Base.(:) is actually a broadcasted call. That means it’s interpreted as Base being the callable (it’s not, hence the error) and : being the iterable (it’s not) whose elements are put into a call in each iteration. Here’s an example of a broadcasted call (that works):

julia> typeof.( [1, 1.0, "one"] )
3-element Vector{DataType}:
 Int64
 Float64
 String

Incidentally, these are what you do instead of an accidental broadcasted call :

julia> :, Base.:(:), Base.Colon()
(Colon(), Colon(), Colon())

It’s analogous to these for the addition operator, except the parser let you omit the parentheses in this case:

julia> +, Base.:(+), Base.:+
(+, +, +)

I don’t expect this to be useful in practice, though. Since Base is imported by default and exports :, as pointed out already, you should just use :. The parser also gets in the way of you trying to assign a : variable in different modules, so you’ll never need to do NotBase.:(:) to distinguish it from Base.:(:). I was wrong, you can assign it, you just need atypical parentheses so the parser doesn’t misinterpret it.

julia> module X  (:) = 0  end;

julia> X.:(:)
0
2 Likes

Thanks a lot!

I had so many mistakes I didn’t know about or didn’t get to catching yet.

Lastly, for performance reasons you probably want to keep the type of the variables i and j constant.

I realized now that I had to write methods to include the Colon type, otherwise it kept giving me an error when I would test for it. My current solution is

function Base.getindex(x::NamedMatrix, i::Union{Vector{<:Any}, Colon}, j::Union{Vector{<:Any}, Colon})
    if !isequal(i, :)
        i = indexin(i, x.rownames)
    end
    if !isequal(j, :)
        j = indexin(j, x.colnames)
    end
    NamedMatrix(x.matrix[i, j],
        x.rownames[i],        
        x.colnames[j])
end

And on this point,

Just do x.matrix[i, j] , not sure what were you aiming for here.

I wanted the returned matrices to retain the index names, which is the (useful) behavior in R. R matrices are also mutable as well. In any case, I’m trying to port a library I wrote in R that inherits from its matrix class and this indexing behavior I find very useful.

Very informative addition, thanks.

The Module.:(Symbol) syntax would not have been intuitive but good to know more generally for non-base packages.

The parser also gets in the way of you trying to assign a : variable in different modules

Sad that you cannot overload it though.

I was wrong earlier, you can assign a : variable. That’s not necessarily the same as overloading, though, it depends on if you explicitly used or imported : from Base. If you don’t, the : variable is totally separate from Base.:(:). This is an example of overloading the : in Base:

julia> :
(::Colon) (generic function with 15 methods)

julia> Base.:(:)() = 0

julia> :
(::Colon) (generic function with 16 methods)

Here’s another in a fresh session:

julia> :
(::Colon) (generic function with 15 methods)

julia> import Base: :

julia> (:)() = 0
(::Colon) (generic function with 16 methods)

Don’t actually do these methods, though, you should have 2-3 arguments with custom types to properly use the : syntax.

1 Like

Ah, very well.

I don’t have plans to “overload” or add methods to this operator at the moment but was disappointed to think that there were limitations on this kind of flexibility in the language.

Not sure why you’re trying to return a NamedMatrix, just return a Tuple instead.

module NamedMatrices

export NamedMatrix

struct NamedMatrix{T, I, J}
  matrix::Matrix{T}
  rownames::Vector{I}
  colnames::Vector{J}
end

# Safe construction. Some would use an inner constructor for this purpose.

"""
Public but not exported. Use for constructing named matrices.
"""
function construct(
  m::AbstractMatrix{T}, r::AbstractVector{I}, c::AbstractVector{J},
) where {T, I, J}
  (length(r) == first(size(m))) || error("row count mismatch")
  (length(c) == last(size(m)))  || error("column count mismatch")
  NamedMatrix{T, I, J}(Matrix{T}(m), Vector{I}(r), Vector{J}(c))
end

# TODO: `helper` is not a good name

helper(::Colon, ::AbstractVector) = (:)

# The coercion ensures type stability.
helper(s, v::AbstractVector) = findfirst(==(s), v)::Int

const Arg = Union{T, Colon} where {T}

function Base.getindex(
  x::NamedMatrix{T, I, J}, s::Arg{I}, r::Arg{J},
) where {T, I, J}
  i = helper(s, x.rownames)
  j = helper(r, x.colnames)
  (x.matrix[i, j], s, r)
end

end

Usage example:

julia> include("/tmp/NamedMatrices.jl")
Main.NamedMatrices

julia> using .NamedMatrices

julia> m = NamedMatrices.construct(rand(2, 3), ["firstRow", "secondRow"], [2//3, 1//3])
ERROR: column count mismatch
Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:35
 [2] construct(m::Matrix{Float64}, r::Vector{String}, c::Vector{Rational{Int64}})
   @ Main.NamedMatrices /tmp/NamedMatrices.jl:20
 [3] top-level scope
   @ REPL[3]:1

julia> m = NamedMatrices.construct(rand(2, 3), ["firstRow", "secondRow"], [3//3, 2//3, 1//3])
NamedMatrix{Float64, String, Rational{Int64}}([0.9164005697901872 0.2696022324777293 0.5365684417224075; 0.4401606748214111 0.7102419990453707 0.7351640896998047], ["firstRow", "secondRow"], Rational{Int64}[1, 2//3, 1//3])

julia> m["firstRow", 2//3]
(0.2696022324777293, "firstRow", 2//3)