How can I access the body of a function?
Context: I have functions inside modules which I execute with specific parameter values. I want to "keep a record" of these parameter values and corresponding functional forms. Below my attempt:
module MyModule
using Parameters # Parameters provides unpack() macro
using DataFrames # DataFrames used to store results in a DataFrame
struct ModelParameters
γ::Float64
U::Function
end
function ModelParameters(;
γ = 2.0,
U = c -> if γ == 1.0; log(c); else (c^(1-γ)-1)/(1-γ) end
)
ModelParameters(γ, U)
end
function show_constants(mp::ModelParameters)
#unpack γ = mp
d = DataFrame(
Name = ["γ"],
Description = ["parameter of U"],
Value = [γ]
)
return(d)
end
function show_functions(mp::ModelParameters)
#unpack U = mp
d = DataFrame(
Name = ["U"],
Description = ["function with parameter γ"],
Value = [U]
)
return d
end
export
ModelParameters
show_constants,
show_functions
end # end of MyModule
Keeping a record:
using Main.MyModule
mp = ModelParameters()
MyModule.show_constants(mp)
1×3 DataFrame
Row │ Name Description Value
│ String String Float64
─────┼─────────────────────────────────
1 │ γ parameter of U 2.0
MyModule.show_functions(mp)
1×3 DataFrame
Row │ Name Description Value
│ String String #2#4…
─────┼──────────────────────────────────────────
1 │ U function with parameter γ #2
This is quite useful to store scalars and arrays value, but not for functions. How could I replace #2 with something useful?
Examples of what would be useful:
c -> if γ == 1.0; log(c); else (c^(1-γ)-1)/(1-γ) end,
or
(c^(1-2.0)-1)/(1-2.0)
or (magically simplified):
1-c^(-1.0)
My question is somewhat related to Julia: show body of function (to find lost code).
You can find a similar discussion here, the best solution for functions that fit in a single line in my opinion is something like this:
type mytype
f::Function
s::String
end
mytype(x::String) = mytype(eval(parse(x)), x)
Base.show(io::IO, x::mytype) = print(io, x.s)
instead of handing over a function as an expression you give it as a String:
t = mytype("x -> x^2")
you call the function like this
t.f(3)
and access the String representation like this:
t.s
Related
The code at the end of this post constructs a function which is bound to the variables of a given dictionary. Furthermore, the function is not bound to the actual name of the dictionary (as I use the Ref() statement).
An example:
julia> D = Dict(:x => 4, :y => 5)
julia> f= #mymacro4(x+2y, D)
julia> f()
14
julia> DD = D
julia> D = nothing
julia> f()
14
julia> DD[:x] = 12
julia> f()
22
Now I want to be able to construct exactly the same function when I only have access to the expression expr = :(x+2y).
How do I do this? I tried several things, but was not able to find a solution.
julia> f = #mymacro4(:(x+2y), D)
julia> f() ### the function evaluation should also yield 14. But it yields:
:(DR.x[:x] + 2 * DR.x[:y])
(I actually want to use it within another macro in which the dictionary is automatically created. I want to store this dictionary and the function within a struct, such that I'm able to call this function at a later point in time and manipulate the objects in the dictionary. If necessary, I may post the complete example and explain the complete problem.)
_freevars2(literal) = literal
function _freevars2(s::Symbol)
try
if typeof(eval(s)) <: Function
return s
else
return Meta.parse("DR.x[:$s]")
end
catch
return Meta.parse("DR.x[:$s]")
end
end
function _freevars2(expr::Expr)
for (it, s) in enumerate(expr.args)
expr.args[it] = _freevars2(s)
end
return expr
end
macro mymacro4(expr, D)
expr2 = _freevars2(expr)
quote
let DR = Ref($(esc(D)))
function mysym()
$expr2
end
end
end
end
I want to generate data from a linear model with noise (Y = X*w + e) where I can specify the distributions of input vector X and scalar noise e. For this, I specify the below struct
using Distributions
struct NoisyLinearDataGenerator
x_dist::ContinuousMultivariateDistribution
noise_dist::ContinuousUnivariateDistribution
weights::Vector{Float64}
end
and a function to generate N points from it:
function generate(nl::NoisyLinearDataGenerator, N)
x = rand(nl.x_dist, N)'
e = rand(nl.noise_dist, N)
return x, x*nl.weights + e
end
This seems to be working, however not type stable, as
nl = NoisyLinearDataGenerator(MvNormal(5, 1.0), Normal(), ones(5))
#code_warntype generate(nl,1)
yields
Variables
#self#::Core.Compiler.Const(generate, false)
nl::NoisyLinearDataGenerator
N::Int64
x::Any
e::Any
Body::Tuple{Any,Any}
1 ─ %1 = Base.getproperty(nl, :x_dist)::Distribution{Multivariate,Continuous}
│ %2 = Main.rand(%1, N)::Any
│ (x = Base.adjoint(%2))
│ %4 = Base.getproperty(nl, :noise_dist)::Distribution{Univariate,Continuous}
│ (e = Main.rand(%4, N))
│ %6 = x::Any
│ %7 = x::Any
│ %8 = Base.getproperty(nl, :weights)::Array{Float64,1}
│ %9 = (%7 * %8)::Any
│ %10 = (%9 + e)::Any
│ %11 = Core.tuple(%6, %10)::Tuple{Any,Any}
└── return %11
I am not sure why this is, since I would expect the type of the sampled data to be specified by using ContinuousMultivariateDistribution and ContinuousUnivariateDistribution.
What is leading to type instability here and what should a type stable implementation look like?
The problem is that ContinuousMultivariateDistribution and ContinuousUnivariateDistribution are abstract types. While your knowledge of statistics tells you that they probably should return Float64, there is no guarantee on a language level that someone won't implement, say, a ContinuousUnivariateDistribution that returns some other object. Therefore the compiler can't know that all ContinuousUnivariateDistribution produces any particular type.
For example, I might write:
struct BadDistribution <: ContinuousUnivariateDistribution end
Base.rand(::BadDistribution, ::Integer) = nothing
Now, you could make a NoisyLinearDataGenerator containing a BadDistribution as x_dist. What would the output type be then?
In other words, the output of generate simply can't be predicted only from its input types.
To solve this, you need to either specify specific distributions for your new type, or else make your new type parametric. In Julia, whenever we have a field of a type that cannot be specified to a concrete type, we usually leave it as a type parameter. Thus, one possible solution is this:
using Distributions
struct NoisyLinearDataGenerator{X,N}
x_dist::X
noise_dist::N
weights::Vector{Float64}
function NoisyLinearDataGenerator{X,N}(x::X, n::N, w::Vector{Float64}) where {
X <: ContinuousMultivariateDistribution,
N <: ContinuousUnivariateDistribution}
return new{X,N}(x,n,w)
end
end
function NoisyLinearDataGenerator(x::X, n::N, w::Vector{Float64}) where {
X <: ContinuousMultivariateDistribution,
N <: ContinuousUnivariateDistribution}
return NoisyLinearDataGenerator{X,N}(x,n,w)
end
function generate(nl::NoisyLinearDataGenerator, N)
x = rand(nl.x_dist, N)'
e = rand(nl.noise_dist, N)
return x, x*nl.weights + e
end
nl = NoisyLinearDataGenerator(MvNormal(5, 1.0), Normal(), ones(5))
Here, the type of nl is NoisyLinearDataGenerator{MvNormal{Float64,PDMats.ScalMat{Float64},FillArrays.Zeros{Float64,1,Tuple{Base.OneTo{Int64}}}},Normal{Float64}} (yes, I know, awful to read), but its type contain all information needed for the compiler to fully predict the output type of generate.
I have a module with an enum defined in it.
module myModule
#enum type A B B C D
end
type1 = myModule.A
Now I want to declare an instance of this enum type but I only have a string specifying which type it is. I tried the following:
str = "B"
type2 = eval(:(myModule.Symbol($str)))
But I get a warning message which I do not quite understand:
WARNING: replacing module myModule.
and the type of type2 is also just a Symbol.
Probably the simplest way is to use getproperty:
julia> module myModule
#enum type A B C D
end
Main.myModule
julia> str = "B";
julia> getproperty(myModule, Symbol(str))
B::type = 1
Alternatively, you could create your expression as a string and then parse and evaluate it:
julia> eval(Meta.parse(string("myModule.", str)))
B::type = 1
Or, the same thing, but with string interpolation instead of using the string function:
julia> eval(Meta.parse("myModule.$str"))
B::type = 1
Note that the syntax myModule.Symbol(str) is not equivalent to myModule.B. It looks like that syntax really just calls Symbol(str) in the global scope. For example, try the following:
julia> myModule.length([1, 2, 3])
3
julia> #code_lowered myModule.length([1, 2, 3])
CodeInfo(
1 ─ %1 = (Base.arraylen)(a)
└── return %1
)
I can unpack a tuple. I'm trying to write a function (or macro) that would unpack a subset of these from an instance of the type-constructor Parameters(). That is, I know how to do:
a,b,c = unpack(p::Parameters)
But I would like to do something like this:
b,c = unpack(p::Parameters, b,c)
or maybe even lazier:
unpack(p::Parameters, b, c)
This is to avoid writing things like:
function unpack_all_oldstyle(p::Parameters)
a=p.a; b=p.b; c=p.c; ... z=p.z;
return a,b,c,...,z
end
There's something wrong with my approach, but hopefully there is a fix.
In case it wasn't clear from the wording of my question, I'm a total ignoramus. I read about unpacking the ellipsis here: how-to-pass-tuple-as-function-arguments
"module UP tests Unpacking Parameters"
module UP
struct Parameters
a::Int64
b::Int64
c::Int64
end
"this method sets default parameters and returns a tuple of default values"
function Parameters(;
a::Int64 = 3,
b::Int64 = 11,
c::Int64 = 101
)
Parameters(a, b, c)
end
"this function unpacks all parameters"
function unpack_all(p::Parameters)
return p.a, p.b, p.c
end
"this function tests the unpacking function: in the body of the function one can now refer to a rather than p.a : worth the effort if you have dozens of parameters and complicated expressions to compute, e.g. type (-b+sqrt(b^2-4*a*c))/2/a instead of (-p.b+sqrt(p.b^2-4*p.a *p.c))/2/p.a"
function unpack_all_test(p::Parameters)
a, b, c = unpack_all(p)
return a, b, c
end
"""
This function is intended to unpack selected parameters. The first, unnamed argument is the constructor for all parameters. The second argument is a tuple of selected parameters.
"""
function unpack_selected(p::Parameters; x...)
return p.x
end
function unpack_selected_test(p::Parameters; x...)
x = unpack_selected(p, x)
return x
end
export Parameters, unpack_all, unpack_all_test, unpack_selected, unpack_selected_test
end
p = UP.Parameters() # make an instance
UP.unpack_all_test(p)
## (3,11,101) ## Test successful
UP.unpack_selected_test(p, 12)
## 12 ## intended outcome
UP.unpack_selected_test(p, b)
## 11 ## intended outcome
UP.unpack_selected_test(p, c, b, a)
## (101,11,3) ## intended outcome
There already exists one: Parameters.jl.
julia> using Parameters
julia> struct Params
a::Int64
b::Int64
c::Int64
end
julia> #unpack a, c = Params(1,2,3)
Params(1,2,3)
julia> a,c
(1,3)
julia> #with_kw struct Params
a::Int64 = 3
b::Int64 = 11
c::Int64 = 101
end
julia> #unpack c,b,a = Params()
Params
a: Int64 3
b: Int64 11
c: Int64 101
julia> c,b,a
(101,11,3)
BTW, you can fix your unpack_selected by:
unpack_selected(p::Parameters, fields...) = map(x->getfield(p, x), fields).
# note that, the selected field names should be Symbol here
julia> unpack_selected(p, :b)
(11,)
julia> unpack_selected(p, :c, :b, :a)
(101,11,3)
I defined a higher-order function like this:
val func : int -> string -> unit
I would like to use this function in two ways:
other_func (func 5)
some_other_func (fun x -> func x "abc")
i.e., by making functions with one of the arguments already defined. However, the second usage is less concise and readable than the first one. Is there a more readable way to pass the second argument to make another function?
In Haskell, there's a function flip for this. You can define it yourself:
let flip f x y = f y x
Then you can say:
other_func (func 5)
third_func (flip func "abc")
Flip is defined in Jane Street Core as Fn.flip. It's defined in OCaml Batteries Included as BatPervasives.flip. (In other words, everybody agrees this is a useful function.)
The question posed in the headline "change order of parameters" is already answered. But I am reading your description as "how do I write a new function with the second parameter fixed". So I will answer this simple question with an ocaml toplevel protocol:
# let func i s = if i < 1 then print_endline "Counter error."
else for ix = 1 to i do print_endline s done;;
val func : int -> string -> unit = <fun>
# func 3 "hi";;
hi
hi
hi
- : unit = ()
# let f1 n = func n "curried second param";;
val f1 : int -> unit = <fun>
# f1 4;;
curried second param
curried second param
curried second param
curried second param
- : unit = ()
#