I want to write a function in Julia that can take any composite type and pretty-print the names of the nested members, their types and their values, and I want to put this function in a package for the community to use.
Imagine a user has the following structs:
struct House
value::Int32
rooms::Int32
number::Int32
end
struct Street
name::String
houses::AbstractArray{House}
end
struct Town
name::String
streets::AbstractArray{Street}
end
town = Town(<initialization code here>)
This user can call PrettyPrinter.print(town), which should output something like
town::Town
name::String = "London"
streets[1]::Street
name::String = "Downing Street"
houses[1]::House
value::Int32 = 100000
rooms::Int32 = 5
number::Int32 = 10
houses[2]::House
value::Int32 = 300000
rooms::Int32 = 6
number::Int32 = 40210
But of course, the PrettyPrinter package can not parse the struct implementation, it has to do its job by low level Julia trickery. My problem is not with the recursive programming.
Question: How do I access the member names, their types and their values?
This functionality is already built-in into dump function:
julia> t = Town("London", [Street("Downing Street",[House(100000,5,10),House(300000,6,40210)])])
Town("London", Street[Street("Downing Street", House[House(100000, 5, 10), House(300000, 6, 40210)])]);
julia> dump(t)
Town
name: String "London"
streets: Array{Street}((1,))
1: Street
name: String "Downing Street"
houses: Array{House}((2,))
1: House
value: Int32 100000
rooms: Int32 5
number: Int32 10
2: House
value: Int32 300000
rooms: Int32 6
number: Int32 40210
However, if you want query the data, you can use the fieldnames function:
julia> fieldnames(typeof(t))
(:name, :streets)
julia> getfield(t, :name)
"London"
Related
I have the following mutable struct:
mutable struct foo
x::Vector{String}
# other field(s)...
# constructor(s)
end
I would like to create an object from this struct and edit it like the following:
bar = foo() # or some other constructor
push!(bar.x, "a")
# Do some stuff...
push!(bar.x, "b")
# Do some more stuff...
push!(bar.x, "c")
To do this, what is the best constructor for foo?
P.S. I have tried the following constructors:
foo() = new()
If I use this and do push!(bar.x, "a"), I get an UndefRefError. I could initialize bar.x outside (like bar.x = []), but I really want to avoid this.
I have also tried:
foo() = new([])
With this, I can do the push operations without a problem. However, if I have many other other fields in the struct that are also vectors, I would have to do this:
mutable struct foo
x::Vector{String}
y::Vector{String}
z::Vector{String}
w::Vector{String}
# maybe dozens more...
foo() = new([], [], [], [], ...) # kind of ugly and wastes time to type
end
Is this the best there is?
You can do:
julia> mutable struct Foo
x::Vector{String}
Foo() = new(String[])
end
julia> Foo()
Foo(String[])
However, for your scenario the most convenient would be Base.#kwdef:
Base.#kwdef mutable struct FooB
x::Vector{String} = String[]
y::Vector{String} = String[]
end
Now of course you can do:
julia> FooB()
FooB(String[], String[])
However other methods are available too:
julia> methods(FooB)
# 3 methods for type constructor:
[1] FooB(; x, y) in Main at util.jl:478
[2] FooB(x::Vector{String}, y::Vector{String}) in Main at REPL[9]:2
[3] FooB(x, y) in Main at REPL[9]:2
So you could do:
julia> FooB(;y=["hello","world"])
FooB(String[], ["hello", "world"])
There's one alternative that is quite concise and almost as fast as writing out the inputs:
struct Foo # Typenames should be uppercase
x::Vector{String}
y::Vector{String}
z::Vector{String}
w::Vector{String}
Foo() = new((String[] for _ in 1:4)...) # using a generator and splatting
# Foo() = new(ntuple(_->String[], 4)...) # also possible
end
Let's say, I have a discriminated union type AccountEvent and a class Aggregate that carries two methods:
Apply1(event : AccountEvent)
Apply2(event : Event<AccountEvent>)
Event<'TEvent> being just a dummy class for sake of having a generic type.
I am trying to create an Expression that represents the call to Apply1 and Apply2 supporting for the parameter type the Discriminated union case type.
That is allowing:
AccountEvent.AccountCreated type for Apply1
Event<AccountEvent.AccountCreated> type for Apply2
I want to achieve that without changing the signature of Apply1, Apply2 and the definition the discriminated union.
The code
type AccountCreation = {
Owner: string
AccountId: Guid
CreatedAt: DateTimeOffset
StartingBalance: decimal
}
type Transaction = {
To: Guid
From: Guid
Description: string
Time: DateTimeOffset
Amount: decimal
}
type AccountEvent =
| AccountCreated of AccountCreation
| AccountCredited of Transaction
| AccountDebited of Transaction
type Event<'TEvent>(event : 'TEvent)=
member val Event = event with get
type Aggregate()=
member this.Apply1(event : AccountEvent)=
()
member this.Apply2(event : Event<AccountEvent>)=
()
let createExpression (aggregateType: Type)(eventType: Type)(method: MethodInfo) =
let instance = Expression.Parameter(aggregateType, "a")
let eventParameter = Expression.Parameter(eventType, "e")
let body = Expression.Call(instance, method, eventParameter)
()
[<EntryPoint>]
let main argv =
let accountCreated = AccountEvent.AccountCreated({
Owner = "Khalid Abuhakmeh"
AccountId = Guid.NewGuid()
StartingBalance = 1000m
CreatedAt = DateTimeOffset.UtcNow
})
let accountCreatedType = accountCreated.GetType()
let method1 = typeof<Aggregate>.GetMethods().Single(fun x -> x.Name = "Apply1")
createExpression typeof<Aggregate> typeof<AccountEvent> method1
createExpression typeof<Aggregate> accountCreatedType method1
let method2 = typeof<Aggregate>.GetMethods().Single(fun x -> x.Name = "Apply2")
let eventAccountCreatedType = typedefof<Event<_>>.MakeGenericType(accountCreatedType)
createExpression typeof<Aggregate> typeof<Event<AccountEvent>> method2
createExpression typeof<Aggregate> eventAccountCreatedType method2
0
With my current solution it does not work to generate an expression for Apply2:
System.ArgumentException: Expression of type 'Program+Event`1[Program+AccountEvent+AccountCreated]' cannot be used for parameter of type 'Program+Event`1[Program+AccountEvent]' of method 'Void Apply2(Event`1)'
Parameter name: arg0
at at System.Dynamic.Utils.ExpressionUtils.ValidateOneArgument(MethodBase method, ExpressionType nodeKind, Expression arguments, ParameterInfo pi, String methodParamName, String argumentParamName, Int32 index)
at at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, Expression arg0)
at at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, IEnumerable`1 arguments)
at at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, Expression[] arguments)
at Program.doingStuff(Type aggregateType, Type eventType, MethodInfo method) in C:\Users\eperret\Desktop\ConsoleApp1\ConsoleApp1\Program.fs:40
at Program.main(String[] argv) in C:\Users\eperret\Desktop\ConsoleApp1\ConsoleApp1\Program.fs:61
I am wondering how I can adjust the creation of my expression to accept the Event<AccountEvent.AccountCreated>?
I am thinking that maybe there is a need to have an intermediate layer to have a conversion layer from AccountEvent.AccountCreated to its base classAccountEvent (this is how discriminated unions are compiled), or more precisely considering the generic, from Event<AccountEvent.AccountCreated to Event<AccountEvent>.
hard to say if this answers your question.
open System
open System
type AccountCreation = {
Owner: string
AccountId: Guid
CreatedAt: DateTimeOffset
StartingBalance: decimal
}
type Transaction = {
To: Guid
From: Guid
Description: string
Time: DateTimeOffset
Amount: decimal
}
type AccountEvent =
| AccountCreated of AccountCreation
| AccountCredited of Transaction
| AccountDebited of Transaction
type CheckinEvent =
| CheckedIn
| CheckedOut
type Event<'T> = AccountEvent of AccountEvent | OtherEvent of 'T
let ev : Event<CheckinEvent> = AccountEvent (AccountCreated {
Owner= "string"
AccountId= Guid.NewGuid()
CreatedAt= DateTimeOffset()
StartingBalance=0m
})
let ev2 : Event<CheckinEvent> = OtherEvent CheckedOut
let f ev =
match ev with
| AccountEvent e -> Some e
| OtherEvent (CheckedOut) -> None
| OtherEvent (CheckedIn) -> None
let x = f ev
let y = f ev2
afterwards, a match statement like this might simplify all that. Honestly it's a little complicated for me to follow what precisely what you're doing there, but using a function instead of a method and using a match statement appears to accomplish the same goal. Ideally you should probably fully spell out the types in a DU instead of using a generic so that you'll get compile time checks instead of run time errors and can know for certain that your code is fully covered by the compiler.
I have a problem with writing classes in Julia. I have looked at the documentation and I haven't seen any docs on classes.
In Python, classes are, for example,
class Dog:
# ----blah blah---
How is this possible in Julia?
Julia does not have classes. Instead we define new types and then define methods on those types. Methods are not "owned" by the types they operate on. Instead, a method can be said to belong to a generic function of the same name as the method. For instance, there are many versions ("methods") of the length function; together they form the generic function length.
Here's an extended example of the Julian approach to programming with types and methods. New types are declared using the struct keyword:
struct Person
name::String
age::Int64
end
Now we can define methods on the Person type:
name(p::Person) = p.name
age(p::Person) = p.age
bio(p::Person) = println("My name is ", name(p)," and I am ", age(p), " years old.")
Methods can be defined for different combinations of argument types. To illustrate this, let's first define some new types:
abstract type Pet end
struct Cat <: Pet
name::String
color::String
end
name(c::Cat) = c.name
color(c::Cat) = c.color
species(::Cat) = "cat"
struct Dog <: Pet
name::String
color::String
end
name(d::Dog) = d.name
color(d::Dog) = d.color
species(::Dog) = "dog"
bio(p::Pet) = println("I have a ", color(p), " ", species(p), " named ", name(p), ".")
struct Plant
type::String
end
type(p::Plant) = p.type
bio(p::Plant) = println("I have a ", type(p), " house plant.")
At this point we can see that we've defined three different one-argument methods for bio:
julia> methods(bio)
3 methods for generic function "bio":
[1] bio(p::Plant) in Main at REPL[17]:1
[2] bio(p::Person) in Main at REPL[4]:1
[3] bio(p::Pet) in Main at REPL[14]:1
Note the comment in the output of methods(bio): "3 methods for generic function 'bio'". We see that bio is a generic function that currently has 3 methods defined for different function signatures. Now let's add a two-argument method for bio:
function bio(person::Person, possession)
bio(person)
bio(possession)
end
Notice that this function is generic in the possession argument, since the internal call to bio(possession) will work whether the possession is a plant, cat, or dog! So we now have four total methods for bio:
julia> methods(bio)
4 methods for generic function "bio":
[1] bio(p::Plant) in Main at REPL[17]:1
[2] bio(p::Person) in Main at REPL[4]:1
[3] bio(p::Pet) in Main at REPL[14]:1
[4] bio(person::Person, possession) in Main at REPL[18]:1
Now let's create a few instances of our types:
alice = Person("Alice", 37)
cat = Cat("Socks", "black")
dog = Dog("Roger", "brown")
plant = Plant("Boston Fern")
So finally we can test our bio methods:
julia> bio(alice, cat)
My name is Alice and I am 37 years old.
I have a black cat named Socks.
julia> bio(alice, dog)
My name is Alice and I am 37 years old.
I have a brown dog named Roger.
julia> bio(alice, plant)
My name is Alice and I am 37 years old.
I have a Boston Fern house plant.
Side note: Modules are used primarily for namespace management. A single module can contain the definitions for multiple types and multiple methods.
The closest one can get to classes with methods in Julia is the module:
module DogClass
export Dog, bark
struct Dog
name::String
end
function bark(d::Dog)
println(d.name, " says woof!")
end
end #MODULE
using .DogClass # note the . here, means look locally for module, not library
mydog = Dog("Fido")
bark(mydog)
I have a Union{Type1, Type2, Type3}, which matches all values whose type is one of those types. But how do I match the types themselves?
MyU = Union{Float64, Int, Array}
a::MyU = 3.5 # works
a = 5 # works
a = [1, 2, 3] # works
# but of course
a = Float64 # nope
a = Int # nope
a = Array # nope
With normal types this is usually achieved via Type{MyType}, whose only value is MyType. But Type{MyU} matches only MyU, and not the types it contains. How do I match those?
I can of course just use DataType, but this has two issues:
It matches any type, not only those I want.
It doesn't match UnionAll types, like Array.
My current workaround is Union{DataType,UnionAll}, but it's an ugly hack that is additionally bound to crash and burn if I include another Union or some other non-concrete type into MyU.
My other solution is to make a second, parallel Union like so:
MyU = Union{Float64, Int, Array}
MyUT = Union{Type{Float64}, Type{Int}, Type{Array}}
It does work and is more strict, but it's also ugly and introduces large possibility of human error with manually keeping those in sync.
You could consider something like this to avoid macros (which can be tricky):
gettypes(u::Union) = [u.a; gettypes(u.b)]
gettypes(u) = [u]
typewrap(u) = Union{[Type{v} for v in gettypes(u)]...}
and then:
julia> MyU = Union{Float64, Int, Array}
Union{Float64, Int64, Array}
julia> MyUT = typewrap(MyU)
Union{Type{Array}, Type{Float64}, Type{Int64}}
EDIT
As an additional note, you can define gettypes as one liner like this:
gettypes(u) = u isa Union ? [u.a; gettypes(u.b)] : [u]
EDIT 2
Or yet simpler without an intermediate array:
typewrap(u) = u isa Union ? Union{Type{u.a}, typewrap(u.b)} : Type{u}
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)