2

I'm still a beginner with Julia, so apologies for the OOP terminology.

Let X and Y be two user-defined matrices with an element type of MyType. Let I be the identity matrix. MyType, in this case, happens to be an object with a single attribute and all the operations are done through this attribute. I'm trying to test if ((X-I)')^-1 is approximately Y. I want to do this by defining as few functions as possible. But I also don't want MyType to inherit from anything.


I would expect that I would only need to define:

  • one and zero for the identity matrix. The elements of I should know that they are of type MyType.
  • +(x::MyType, y::MyType), -, *, / for matrix multiplication and inversion.
  • adjoint for determining (X-I)'.
  • isapprox to give some kind of meaning.

However, in practice, I need far more than these. Is there any way that I can reduce the number of implemented methods for this?


Here is what I have so far. It took me some time to finally stop getting MethodErrors.

# Define MyType
struct MyType  # Don't inherit from anything
    value
end
value(x::MyType) = x.value

# Define its methods
Base.one(::Type{MyType}) = MyType(1)
Base.zero(::Type{MyType}) = MyType(0)

Base.:+(x::MyType, y::MyType) = MyType(value(x)+value(y))
Base.:*(x::MyType, y::MyType) = MyType(value(x)*value(y))
Base.:-(x::MyType, y::MyType) = MyType(value(x)-value(y))
Base.:/(x::MyType, y::MyType) = MyType(value(x)/value(y))

Base.adjoint(x::MyType) = MyType(adjoint(value(x)))

function Base.isapprox(x::MyType, y::MyType; atol, rtol)
    return isapprox(value(x), value(y), atol=atol, rtol=rtol)
end
function Base.rtoldefault(::Type{MyType}, ::Type{MyType}, z)
    # I don't really want to be restricted to Float64 tolerance.
    return Base.rtoldefault(Float64, Float64, z)
end

# Shouldn't have to define these
Base.one(::MyType) = one(MyType)
Base.zero(::MyType) = zero(MyType)

Base.:+(x::MyType, y) = MyType(value(x)+y)
Base.:*(x::MyType, y) = MyType(value(x)*y)

Base.abs(x::MyType) = MyType(abs(value(x)))
Base.:<(x::MyType, y::MyType) = value(x) < value(y)
Base.inv(x::MyType) = MyType(inv(value(x)))

Base.promote_rule(::Type{Any}, ::Type{MyType}) = MyType
Base.convert(::Type{MyType}, x::MyType) = x
Base.convert(::Type{MyType}, x) = MyType(x)

Base.iterate(x::MyType) = (value(x), nothing)
Base.iterate(::MyType, ::Any) = nothing
Base.length(x::MyType) = 1

# Begin check for ((X-I)')^-1 ≈ Y
X = [
    0.4 1;
    -0.2 0.8;
]
X = map(x -> MyType(x), X)

Y = [
    -0.625 0.625;
    -3.125 -1.875;
]
Y = map(x -> MyType(x), Y)

using LinearAlgebra
println((X-I)')
println(inv((X-I)'))
println(inv((X-I)') ≈ Y)

I know I can use macros to make this easier. But this is not the point of the exercise.

I'm sure I've made many stupid mistakes particularly with promote_rule and iterate. I've tried reading the documentation multiple times, so I must be pretty stupid.

Any help much appreciated.

Chris du Plessis
  • 1,250
  • 7
  • 13
  • By "not inheriting", do you mean you don't want it to be a subtype of anything (like, e.g., `Real` or `Number`? – Benoit Pasquier Dec 15 '20 at 06:54
  • @BenoitPasquier Yes. It should only be a subtype of `Any`. – Chris du Plessis Dec 15 '20 at 07:00
  • 1
    Do you mind me asking why? It looks like you want it to behave like a `Number` but not be one. Although this is perfectly valid, I'm curious why you would want that :) – Benoit Pasquier Dec 15 '20 at 11:48
  • @BenoitPasquier It's mostly for user experience. I've made a library that currently works with `Rational`, `Complex`, etc and I would like to extend it to just about any type. I've got it working with array types but I still want to get it working for user-defined types. Then I can tell the user what methods they need to implement in order for them to use my library with their own types (or even types from other libraries). – Chris du Plessis Dec 15 '20 at 12:52
  • 1
    You can spare yourself two lines with `Base.one(::Union{Type{MyType},MyType}) = MyType(1)` and same for zero and add something like `mytypematrix(x::Matrix) = [MyType(value) for value in x]` to avoid having to map a Matrix every time – Oskar Dec 15 '20 at 20:21

1 Answers1

1

The absence of fallback methods is sometimes an omission (and you could submit a pull request to fix that) and sometimes deliberate. Let's take the case of zero: why isn't there a fallback zero(x) = zero(typeof(x)), so that you wouldn't have to write that yourself? Well, obviously that wouldn't be hard to add, and one could argue that we should have it. But consider what happens if a user creates a new type and then ends up needing an undefined method:

julia> struct MyMatrix{T}
           buffer::Vector{T}
           sz::Tuple{Int,Int}
       end

julia> M = MyMatrix(rand(9), (3, 3));

julia> zero(M)
ERROR: MethodError: no method matching zero(::MyMatrix{Float64})
Closest candidates are:
  zero(::Union{Type{P}, P}) where P<:Dates.Period at /home/tim/src/julia-master/usr/share/julia/stdlib/v1.7/Dates/src/periods.jl:53
  zero(::T) where T<:Dates.TimeType at /home/tim/src/julia-master/usr/share/julia/stdlib/v1.7/Dates/src/types.jl:423
  zero(::SparseArrays.AbstractSparseArray) at /home/tim/src/julia-master/usr/share/julia/stdlib/v1.7/SparseArrays/src/SparseArrays.jl:55
  ...
Stacktrace:
 [1] top-level scope
   @ REPL[3]:1

Now, I can easily imagine figuring out how to write this method: I have the element type, I have the size, so I am all set. Suppose instead we'd defined the fallback:

julia> Base.zero(M::MyMatrix) = zero(typeof(M))

julia> zero(M)
ERROR: MethodError: no method matching zero(::Type{MyMatrix{Float64}})
Closest candidates are:
  zero(::Union{Type{P}, P}) where P<:Dates.Period at /home/tim/src/julia-master/usr/share/julia/stdlib/v1.7/Dates/src/periods.jl:53
  zero(::MyMatrix) at REPL[4]:1
  zero(::T) where T<:Dates.TimeType at /home/tim/src/julia-master/usr/share/julia/stdlib/v1.7/Dates/src/types.jl:423
  ...
Stacktrace:
 [1] zero(M::MyMatrix{Float64})
   @ Main ./REPL[4]:1
 [2] top-level scope
   @ REPL[5]:1

OK, so now I sit down to implement zero(::Type{MyMatrix{T}}) ... and realize that I have no idea what size of matrix I should be creating. If I don't realize that I should not take the error message so literally, I'm really in trouble now.

With a Number, you don't need any additional parameters to know what zero of that type should mean, but that's not necessarily true of everything. This highlights an advantage in having only sensible fallbacks. What that means in practice is a bit murky, but subtyping gives you one way of expressing your intuition for what's sensible.

tholy
  • 11,882
  • 1
  • 29
  • 42
  • Thank you for the explanation. So just to confirm, the consensus is that either I should subtype something like `Number` or live with the extra methods? – Chris du Plessis Dec 16 '20 at 18:15
  • I agree with that design decision and I was facing a similar conundrum with `Array{Any,2}`. I decided to have `zero(::Type{Array{T,2}}) where T = LinearAlgebra.UniformScaling(0)`. This worked perfectly well. But `zeros` complained that the return type from `zero` was not `Array{T,2}`. Is there a design reason why `zero` is lenient with its return type but `zeros` is not? Also, with a similar problem, `zero([1 2; 3 4])` works but `zeros([1 2; 3 4], 2)` does not. Surely this could be improved as well? – Chris du Plessis Dec 16 '20 at 18:17