Learning Julia by (ab)using constructor

I’ve just been trying to pick up Julia this morning.
And I thought, what’s better than abusing so struct behaves like Python class!

Totally understand things are preferably immutable, and methods declaration are external to structs etc. But just for the sake of curiosity and learning:

  1. Is there a more succinct way to define the inner constructor to achieve the same as below code?
  2. What’s the meaning of defining variables or functions in the struct (but outside of constructor) scope? Only to be used in inner constructor?
julia> mutable struct C
           id::String
           speak::Function
           self::C

           f(self::C, word::String) = println("I'm $(self.id) yeah. $word.")
           function C(id::String) 
               instance = new(id)
               instance.speak = (word::String) -> f(instance, word)
               instance.self = instance
           end
       end

julia> julia = C("julia")
C("julia", var"#5#7"{C,var"#f#6"}(C(#= circular reference @-2 =#), var"#f#6"()), C(#= circular reference @-1 =#))

julia> julia.speak("hey")
I'm julia yeah. hey.

julia> julia.id = "eve"
"eve"

julia> julia.speak("yo")
I'm eve yeah. yo.
1 Like

Welcome!

You can use @with_kw from Parameters.jl to define your default values like:

@with_kw mutable struct C
  id::String
  speak::Function = (word) -> println(id, word)
end

(I’ve shortened your example a bit, but hopefully the idea makes sense).

I guess you could use it for that–it’s extremely unusual to define anything inside the struct other than its fields, and I don’t think I’ve ever seen it done in Julia code in the wild. It’s not something I’ve ever needed, particularly because as soon as you start putting your behavior outside of your structs, it doesn’t really make sense.

4 Likes

It’s worth noting that Function is an abstract type, every function has it’s own type which is a subtype of Function. If you want your struct to have concrete storage, you can parameterize it by the type of the function, e.g.

@with_kw mutable struct C{F}
  id::String
  speak::F = (word) -> println(id, word)
end

or you could just abuse getproperty to add methods:

julia> mutable struct C
           id::String
       end

julia> function Base.getproperty(self::C, s::Symbol)
           if s === :speak
               word -> println("I'm $(self.id) yeah. $word.")
           else
               getfield(self, s)
           end
       end

julia> julia = C("julia")
C("julia")

julia> julia.speak("hi")
I'm julia yeah. hi.
1 Like

Thanks fellas! I think I’ve learned more than I set out for.

Less importantly, after looking at Parameters.jl examples and the implementation a bit turns out there’s seems to be no easy way to combine @with_kw with self referential construction

(I was trying to game it so method call outcome changes based on instance state without it being supplied as argument just like Python, thus the self)

The best (without spending a lot of time) I can come up with is something like this but it isn’t nice because I’m redefining speak() which changes the type so it only works when it’s abstract:

using Parameters

@with_kw mutable struct J
  id::String
  self::Union{J, Nothing} = nothing
  speak::Function = (self::J, word::String) -> println(self.id, word)
  J(id, self, speak) = (ins = new(id, self, speak); ins.speak = (word) -> speak(ins, word); ins.self = ins)
end

Yeah, you’re really fighting the language here by trying to put behavior inside your structs. Life will be much easier if you let that notion go:

julia> mutable struct J
         id::String
       end

julia> speak(self::J, word::String) = println(self.id, word)
speak (generic function with 1 method)

julia> j = J("hello")
J("hello")

julia> speak(j, "world")
helloworld

Putting the behavior outside of the struct…

  • Eliminates the need to use @with_kw at all
  • Removes the unnecessary self field
  • Allows us to skip writing the constructor
  • Allows other implementations of speak for other types to be implemented later.
  • Improves performance by avoiding the entire issue of the abstract speak::Function field type.
5 Likes

This is of course true if your goal is to achieve something productive. Sometimes it’s fun and educational to tinker though.

2 Likes

Absolutely, just gaming it to learn as said.

For a real life use case of self referential struct having default value I think it might be useful for certain kind of tree nodes say?

Anyway.
Love to see the language can indeed express obscure things outside of idiomatic approach with relative ease.
Love to see I’m getting reply here. Thank you.
And of course multi dispatch, which is why I’m drawn here. (I started using Kotlin at work a year ago, think it’s amazing, a bit more functional, less OO, If only it supports multi dispatch, would get rid of a lot of “design patterns” - boiler plates)

I think my next project would most likely be in Julia. Even for a case like backend web service, I have a feeling the pros would outweigh the cons, e.g. comparatively less mature frameworks (routes + authn/z + openapi code generation, cloud clients etc…), I can see the pieces out there just expecting there would be more work to stitches them together or help patch things up.

:heart:

1 Like