Does julia have a construct analogous to Racket's parameters?

Racket has this cool thing where you can define implicit values called parameters: 4.13 Dynamic Binding: parameterize. Is there anything analogous in julia?

For example, it would be convenient in the implementation of things random number generators to give the feeling of having ethereal globals, while in reality offering the programmer more control and healthy scoping rules.

Eg,

# Reach into the either and pull out the `rng` parameter.
foo(x) = randn(@getparam :rng) * 3 + x

# Provide a value for the `rng` parameter but only in the context of this scope.
@provideparam rng = MersenneTwister(...) in begin
  foo(12)
end
1 Like

First thought is, what’s the advantage of doing this vs passing in an explicit random seed?

Well this has the advantage that you can interop with third-party code that doesn’t know anything about random seeds. But my use case is a bit more interesting anyhow. I have a custom numeric type Foo and it contains a reference to a “semi-global” World object:

struct Foo
  world  # pointer to something interesting and contextual
  value  # just a Float64 perhaps
end

Now I’d like to use this with a library like IntervalAnalysis.jl where the lower/upper bounds of my Intervals are Foos… But IntervalAnalysis.jl calls zero() internally. Now the question is how do I implement zero(::Type{Foo})?

With something like parameters I could do:

mkFoo(x) = Foo(@getparam :world, x)
zero(::Type{Foo}) = mkFoo(0.0)

@provideparam world = World(...) in begin
  # My codebase: set up some Foo's, etc
  x = mkFoo(3)
  y = mkFoo(5)

  # Third-party code: use IntervalAnalysis.jl
  interval(x, y)
  ...
end

# And outside the scope, if the paramter isn't provided...
mkFoo(7)
# error: no value provided for parameter `world`
1 Like

interesting. I think using Cassette.jl and overdub you can achieve something like this.

This is an interesting idea and probably Casette.jl or IRTools.jl is the best way to solve it.

On the other hand, to some extent it can be solved with usual macros (sorry, I am just thinking aloud, not sure if its doable in reality).

Idea is that your code roughly equivalent to

struct Foo
  world  # pointer to something interesting and contextual
  value  # just a Float64 perhaps
end


let world = "hello"
    mkFoo(x) = Foo(world, x)
    println(mkFoo(1))
end

i.e. if we create closure inside let block, than we are good to go. Of course, your example use function declaration outside of let block, in which case you can’t declare closure.

What can be done, is “storing” closure declaration somewhere and applying it afterwards, so for the end user, it looks like

@with_context world mkFoo(x) = Foo(world, x)

@in_context world = World(...) begin
  x = mkFoo(3)
  y = mkFoo(5)
...
end

And macro @in_context should generate these lines

let world = World(...) begin
  mkFoo(x) = mkFoo(world, x) # substitute from @with_context declaration

Of course, it’s very quickly became rather complicated: what should we do if we have more than one context? How can we decide which functions to use? And what can we do with nested @in_context?

So it looks like that building full scaled implementation in this approach is too complicated, but limited versions could exist.

What about

julia> struct Foo{World} 
       value
       end

julia> Base.zero(x::Foo{W}) where W = Foo{W}(0)

julia> zero(Foo{1}(10))
Foo{1}(0)

I’m guessing that if he needed to handle a collection of Foos in some cases he might want to keep homogeneity to avoid type instabilities, so a parameterized Foo would make him loose that.

1 Like

Really interesting!
This smells like the classical factory pattern in the OOP world:
Assume Derived <: Base, if function that works for Base also wants to work for Derived, it must create intermediate values of type Derived. To achieve this, we explicitly pass a constructor function that creates Derived instances. Here, you which to customize what kind of Foo is constructed, which could be interpreted as “subtyping” in a more general sense.

If an explicit constructor is not possible, Foo{world} does the trick.

However, in julia, zero(T) does not need to return exactly T. Assume the meaning of zero is independent of context, you can define a ZeroFoo type that represent the meaning of zero, regardless of the context.

1 Like

This would be very nice! I’m not sure how to make it work in Julia, but this may be due to my own ignorance. The World type is effectively defined as

struct World
  xs::Array{Int64}     # not actually Int64, but you get the idea
end

And thus far Julia has not allowed me to pass such things in as type parameters.

Ah… this could work. It would require a little refactor, but this may be the way to go…

Sounds like the ZeroFoo thing may be the cleaner solution, but the original question is interesting and maybe worth pointing out that as long as your code is single-task, it can be done pretty cleanly with Base.task_local_storage:

foo(x) = randn(Base.task_local_storage(:rng)) * 3 + x

Base.task_local_storage(:rng, MersenneTwister(1)) do
  foo(12)
end

(And for asynchronous code, the question may be kind of ambiguous anyway, e.g. what should happen if the code inside @provideparam spawns code which runs after you’ve exited the block)

2 Likes

But this is julia and you can pass a function as type parameter (which seems to be abusing and may not be recommended in general):

struct Foo{world_func} # world_func() returns whatever you like
  value
end
value(x::Foo) = x.value
world(x::Foo{f}) where f = f() # instead of x.world

Note: this is a generalization of the solution @ggggggggg has provided.

2 Likes

Oh I see, if you do not need thread safety, this can be easily implemented:

const global_params = Dict{Symbol, Any}()

function parameterize(f; params...)
    old_values = []
    for (k, v) in params
        push!(old_values, get(global_params, k, missing))
        global_params[k] = v
    end
    try
        f()
    finally
        for ((k, _), v) in zip(params, old_values)
            if ismissing(v)
                delete!(global_params, k)
            else
                global_params[k] = v
            end
        end
    end
end

get_parameter(key::Symbol) = global_params[key]

macro get_parameter(key::Symbol)
    :(get_parameter($(Meta.quot(key))))
end

make_parameter(key::Symbol, value) = global_params[key] = value

function make_parameter(; params...)
    for (k, v) in params
        make_parameter(k, v)
    end
end

Usage:

make_parameter(a=1)
println(get_parameter(:a)) # 1

f = parameterize(a=2, b=3) do
    @show get_parameter(:a)
    @show @get_parameter b
    x -> x + @get_parameter a
end
f(1) # 2

1 Like