Variable interpolation

Hi, everyone. New to Julia and trying to do something like the following:

v1 = "m" 

function mat()
    return "I am a mat"
end 

I would like to be able to issue the following command:
($v1)at

and get back “I am a mat”

However, my feeble attempt at interpolating with ($) doesn’t seem to fly. Do I need a macro for this?

You can use eval(Meta.parse("$(v1)at()")), for example.

However, this sounds like a classic XY problem, in which you are reaching for the wrong tool for the job. What are you ultimately trying to accomplish?

In general, you almost never need to pass functions or expressions as strings — Julia has much better ways to deal with functions as variables. See also How one can evaluate @printf to be returned to a value - #6 by stevengj

1 Like

Okay, I’ll bite!

This is part of an agents.jl model that moves patients through a hospital emergency department based on completion of a series of four lab tests.

The logic for the lab tests is identical across tests, so I wrote a single function that handles whatever test is passed to it. The metaprogramming comes in when I need to update values specific to that test. So:

# get a list of test statuses and choose one at random that is incomplete (status is 1 or 2) 
dxStatus = Dict("ct" => patient.ctStatus, "mri" => patient.mriStatus ,"lab" => patient.labStatus, "xray"=>patient.xrayStatus) 

labName = rand(findall(x-> 0 < x < 3 , dxStatus))

if isempty(labName)
	patient.dxComplete == true 
else
     # pass the testname to the test processing function
     processTest(model, patient,labName)
end 

### the function
function processTest(model, patient,labName)
# check to see if there is space in the lab 
 		if count(x->x.$(labName)Status == 2 ,allagents(model)) < model.$(labName)Capacity
# if capacity, send patient to lab by changing status  in process and noting start time. 
 			patient.$(labName)Status = 2
 			patient.$(labName)StartTime = abmtime(model)
 	end
end

In principle, you could use getfield(x, Symbol(labName * "Status")) instead of x.$(labName)Status.
Imho, using runtime generated names in this fashion is rarely a good idea. Here, I would change the data representation instead and have x.Status hold a dictionary (or some other type that can be indexed) and call it like x.Status[labName] – where labName is now a symbol, e.g., :ct instead of “ct”. This should both be faster and easier to work with than generating fieldnames at runtime.

2 Likes

It looks to me that currently you’re using something like

mutable struct Patient
    ctStatus::Int
    mriStatus::Int
    labStatus::Int
    xrayStatus::Int

    ctStartTime::Float64
    mriStartTime::Float64
    labStartTime::Float64
    xrayStartTime::Float64

    dxComplete::Bool
end

Instead you could (for example) use

mutable struct PatientLabInfo
    status::Int
    start_time::Float64
end

mutable struct Patient
    labs_info::Dict{String, PatientLabInfo}
    dx_complete::Bool
end

and something similar for your Model (type of model).

(I switched the naming scheme to snake_case to make the difference between variables and types clearer, as this can occasionally get confusing for me. But this is not too important. The names themselves might also not be ideal (e.g. PatientLabInfo refers to the information about a Patient some Lab might have). Proper naming is hard without complete understanding of the problem you want to model, and also just in general.)

The lab names (relevant for a given Patient) are then encoded as the keys in the labs_info Dict. The equivalent of your code would be

patient = Patient(
    Dict("ct"   => PatientLabInfo(1, 1.23),
         "mri"  => PatientLabInfo(4, 0.01),
         "lab"  => PatientLabInfo(3, 2.31),
         "xray" => PatientLabInfo(0, 3.21)),
    false
)

lab_name = rand(findall(lab_info -> 0 < lab_info.status < 3, patient.labs_info))
if isempty(lab_name) 
	patient.dx_complete = true   # (You wrote ==, but probably mean = ?)   
else
     # pass the testname to the test processing function
     process_test(model, patient, lab_name)
end 

### the function
function process_test(model, patient, lab_name)
    # check to see if there is space in the lab 
    if count(p->p.labs_info[lab_name].status == 2, allagents(model)) < model.labs_info[lab_name].capacity
        # if capacity, send patient to lab by changing status  in process and noting start time. 
 		patient.labs_info[lab_name].status = 2
 		patient.labs_info[lab_name].start_time = abmtime(model)
 	end
end

By the way, at the moment you check if the lab_name String is empty (i.e. ""), which is presumably never the case? Perhaps you mean that no labs for this patient have status 1 or 2. But in this case the code will already have errored by calling rand on an empty collection. Instead you should call isempty on the result of the findall.

In any case, using this approach you don’t need any string interpolation of lab names into variable names. You simply use the lab names directly as indices in a dictionary.

2 Likes

This seems to get it done. Thanks!


function processTest(model, patient,labName)
	if patient.dxStatus[labName] == 2 
		if (abmtime(model) - patient.dxStartTimes[labName]) > pois_rand(model.dxDurations[labName])
			patient.dxStatus[labName] = 3
			patient.dxEndTimes[labName] = abmtime(model) 
		end
	elseif patient.dxStatus[labName] == 1
		if count(x -> x.dxStatus[labName] == 2 ,allagents(model)) < model.dxCapacities[labName]
			patient.dxStatus[labName] = 2
			patient.dxStartTimes[labName] = abmtime(model)
		else
			nothing ### no lab available
		end
	else
		nothing 
	end
end