Dynamically mutate type parameter

Hello,

I would like to know if there is a way to mutate the type parameter of an object during runtime (dynamically).

MWE:

mutable struct Text{Format<:Union{Integer, AbstractString}}  # For MWE, in reality it is like Union{AbstractString, HTMLDocument, XMLDocument}…
    content::Format
    test_field::Union{Integer, AbstractString}
end

function clean(text::Text{<:AbstractString})
    println("I am a string")
end

function clean(text::Text{<:Integer})  # For MWE, in reality it is like HTMLDocument…
    println("I am an integer")
end

text1 = Text("content", 1)  # Text{String}("content", "string")
clean(text1)  # I am a string
text2 = Text(55, 1)  # Text{Int64}(55, 1)
clean(text2)  # I am an integer
text1.test_field = "string"  # No problem.
text1.content = 55  # ERROR: MethodError: Cannot `convert` an object of type Int64 to an object of type String

In order to avoid to create a new Text object in a recursive function which cleans for example an HTML document (sometimes nodes, sometimes raw text (with source and language type parameters, not shown in this MWE), I prefer to mutate, if possible, the type parameter.
Is it possible (I did not find the syntax to do so), and if not, is there a nice workaround except recreate a Text object which takes the cleaned new content of previous Text object?

Sincerely

It is not possible.

If you don’t care about performance (which is implicitly assumed if you are doing something like this), why don’t you just leave the type of content as either Any or some Union (it should not matter if it contains abstract types), and modify it when needed? Eg

mutable struct Text
   content
   test_field::Union{Integer, AbstractString}
end

text1.content = 55 

etc.

1 Like

Hum, about performance, I must admit I am not sure why you say it is implicitly assumed that I don’t care about it…

If I remove the type parameter, as in your example, the multiple dispatch will not be involved and I will need several tests in different places for nested functions (if text.content isa AbstractString, if text.content isa HTMLNode, etc.), because the methods depend on the type of content.

Because the way Julia works. You get optimized code when concrete types (or small unions of them) are known at compile time. If you want to change this during runtime, you have to live with suboptimal performance (which may still be fine, of course, and this may not be a practical concern).

You may want to read through

https://docs.julialang.org/en/v1/manual/performance-tips/

Make a wrapper function like

f(text) = _f(text.content, text.test_field)
_f(::Integer, ::Integer) = :both_are_integers

etc.

2 Likes

Another option is to use Setfield,

using Setfield
@set! text1.content = 55  # works

Note this doesn’t mutate the original object, but creates a new one and binds it to text1, but is maybe good enough for what you need.

2 Likes

For performances, I tried with abstract and concrete types for my custom type fields and because it was always faster (and with less allocations) with abstract ones, I kept them, even if it was not expected…

Thank you for this module, I think I will one day try both solutions.

@Tamas_Papp OK, after having a bug I just solved because I miscleaned my code after my small abstract versus concrete benchmark, I just remembered why I have chosen to use by default abstract types for my custom type fields and function arguments: to avoid things like Union{Dict, OrderedDict}, Union{String, SubString, Regex}, Union{String, SubString, SubstitutionString}, etc. I could take habit to make these unions, but you said “small unions”, from when does it start to be too big?

There is another reason I used custom abstract types in my case (not shown in the MWE): to avoid mutually circular type declarations (see this issue), because I have 3 custom types (like book, chapter, tale, etc.) all subtypes of respective abstract types (themselves subtypes of one general custom abstract type when I need to refer to all of them in a recursive way, and all of them parameterized), because all children have link field to parent and parents all have a link field vector of children.

So, for former case, I could use union of concrete types, but for latter case, Maybe I can’t avoid abstract types…

I must mention that I almost never work with integers, floats, number matrices, etc. (I never wrote Float in Julia yet), but mainly only with Unicode characters, strings, regexes, etc.