Is Julia lazy?

I don’t think so, better code should be very easy to read.

It’s only hard to read because I went by trial and error. And some of it is really much simpler than writing out everything by ‘special case’. Extending the show method, for example, is so much easier and more powerful than writing a loop over an array. The subscript business is just an extra flourish, not needed.

If you remove the second type parameter, because it’s not needed, the code isn’t that complex.

But using a type parameter for the base is definitely the way to go.

2 Likes

By the way, this is not true at all:

julia> struct MyStruct
         x::Int
       end

julia> function f()
         return MyStruct
       end
f (generic function with 1 method)

julia> f()
MyStruct

julia> f()(5)
MyStruct(5)

Yes, the type of a struct is not generally used as a container for data in Julia in the same way you might use a class-level method or value in Python. However, it’s easy to recreate this effect with multiple dispatch:

julia> function my_property(::MyStruct)
         return 5  # always 5 for any instance of `MyStruct`, effectively a static constant for this type
       end
my_property (generic function with 1 method)

julia> my_property(MyStruct(1))
5
3 Likes

So, it never really occurred to me that elegance was the enemy of true mathematics :upside_down_face: But anyway, I just wanted to show a simpler example:

struct Z{P} <: Integer
    x::Int
    Z{P}(n::Integer) where {P} = new{P}(mod(n, P))
end

Base.promote_rule(::Type{<:Number}, ::Type{Z{P}}) where {P} = Z{P}
Base.show(io::IO, z::Z) = show(io, z.x)

Base.:+(a::Z{P}, b::Z{P}) where {P} = Z{P}(a.x + b.x)
Base.:-(a::Z{P}, b::Z{P}) where {P} = Z{P}(a.x - b.x)
Base.:-(a::Z{P}) where {P} = Z{P}(-a.x)
Base.:*(a::Z{P}, b::Z{P}) where {P} = Z{P}(a.x * b.x)
Base.div(a::Z{P}, b::Z{P}) where {P} = Z{P}(a.x ÷ b.x)  # I don't think this is needed, really

Now you can

jl> z = zeros(Z{7}, 3, 3)
3×3 Matrix{Z{7}}:
 0  0  0
 0  0  0
 0  0  0

jl> one(z)
3×3 Matrix{Z{7}}:
 1  0  0
 0  1  0
 0  0  1

jl> z = Z{11}.(rand(0:10, 3, 3))
3×3 Matrix{Z{11}}:
  3  3   4
  3  4   5
 10  4  10

jl> z.^2 .+ 1
3×3 Matrix{Z{11}}:
 10  10  6
 10   6  4
  2   6  2

jl> maximum(z.^2 .+ 1)
10

jl> (2 + 3im) * Z{11}(2)
4 + 6im

# etc.

I think comparison and random sampling is really neat, but whatever.

Base.:<(a::Z{P}, b::Z{P}) where {P} = (a.x < b.x)
Base.:<=(a::Z{P}, b::Z{P}) where {P} = (a.x <= b.x)
function Random.rand(rng::AbstractRNG, ::Random.SamplerType{Z{P}}) where {P}
    return Z{P}(rand(rng, 0:P-1))
end

Anyway, if you are teaching this to students, you shouldn’t drag macros into it, and for all that is holy, don’t use global(!)

13 Likes

Thank you for this valuable information.

But can you define struct inside body of f?

With experience of other languages behaviour, it’s natural to ask if a new one can do the same and how. But of course I agree that C++ is C++ and Julia is Julia. And in turn I can ask the question : How Julia knows (guesses) that I want the sum and product of matrices with coeffs in F_p be the ‘usual’ sum and product of matrices?

All of the Julia matrix code boils down to operations on the elements. When you use a new element type, Julia will use the appropriate definitions for + and * for that element type. The reason Julia uses multiple dispatch is that it causes stuff like this to “just work”

Exactly. Julia knows how to sum over a matrix: start with the first element, add the second, then the third. And we have just shown Julia how to add integer modulo P.

Same with matrix product: start at one end, multiply, add, repeat. There’s nothing more to it. To improve performance, maybe a specialized matrix product is called for, but that’s just an optimization.

You can, using eval(), but it’s pretty rare to actually need to do so. Do you have a specific use in mind?

2 Likes

Pure curiosity

1 Like

Usually you use named tuples if you want anonymous (on the fly) struct types.

3 Likes

If we can create anonymous struct types in functions, why not named struct types?

I’m not sure, but it is probably related to dispatch. You can dispatch on a type, while a named tuple will dispatch as named tuple.

1 Like

My guess is that it has to do with subtyping. You could imagine creating a struct at the top level, that is a “named struct type” that still allows dispatching on the name of the struct:

struct NamedTupleWithName{N, FN, FT}
    name::N
    fields::NamedTuple{FN, FT}
end

function make(name::Symbol)
    NamedTupleWithName(Val(name), (x=1, y=2))
end

function property(s::NamedTupleWithName{Val{:Foo}})
    s.fields.x + s.fields.y
end

function property(s::NamedTupleWithName{Val{:Bar}})
    s.fields.x * s.fields.y
end
julia> property(make(:Foo))
3

julia> property(make(:Bar))
2

What you don’t have here is a way to encode a subtype relation between different types. For example, I don’t know how you would encode getx from below in the above scheme

abstract type Nonce end
struct Foo <: Nonce
    x
    y
end

struct Bar <: Nonce
    x
    y
end

function property(s::Foo)
    s.x + s.y
end

function property(s::Bar)
    s.x * s.y
end

function getx(s::Nonce)
    s.x
end
1 Like

Yes, I tried, what is the interest ?
test nul(k) replaced by iszero(k) ???
In any case k==0 not understood after such overload. I tried just in case.

k==0 will not call iszero. I think the suggestion is to replace k == 0 with iszero(k).

What I meant was that I think your nul(k) function is a check for whether or not k is zero or not. Julia already has a generic method for this, which is called iszero. This iszero is used throughout Base in various library functions, so if you want to allow them to work with your number type, you can overload Base.iszero(k::F_p) = k.x == 0 instead.

2 Likes

Thank you for your example.
I understand that it’s the way to do it.
I’m not used to this syntax yet and BTW I still have some problems with Julia types.
I have to work on this ; for the moment it’s pure magic.
I hope you won’t mind if I come back to you for a few comments.

Sure, no problem.

1 Like

I think you spent already some time writing your examples. Since you accept to answer some questions I will ask one by one to reach a final comprehension of your code.
Till now I am making some experiments with it, changing this and that trying to generate errors and to interpret these errors. It’s an useful job, but I cannot make everything clear from such experiments.
I would like to be able to write again such kind of code understanding exactly what I’m doing and why.
So first let’s take the first line to begin with :
struct Z{P} <: Integer
By this you describe type Z{P} to be a sub-type of Integer which is an abstract type.
Here are my questions :

  1. What is the benefit doing this ?
  2. What do you loose if you don’t do it ?
  3. What if instead of Integer you choose something higher in the hierarchy such as Real or Number ?
  4. What if you choose instead a primitive concrete type such as Int64 for example ?
    Have a nice day !