🚧🚧🚧
This documentation is completely broken, because I've rearchitected this module from scratch. It's still a WIP. Come back later! 🚧🚧🚧
🚧🚧🚧
Dynamic, Static, and Mutable Objects with Composition, Template Replication, Prototype Inheritance, Method Membership, and Type Tagging for Julia (whew!)
Play with it and see what you think. Prototype inheritance is fun!
Implementation and interface unstable. This is experimental; consider it a fancy digital toy for now.
To install from GitHub:
] add https://github.com/uniment/Objects.jl
and of course,
using Objects
Object
is built to be a catch-all blob to encapsulate associated information, yet to hold that data in a structured manner to maintain high performance.
Instances of Object
have properties that can be casually and easily created and composed, and then accessed and changed using dot-syntax.
Once an object has been carved into the desired form, it can serve as a "template" for replication to create clones with the same datatypes but with custom values.
In addition, objects can inherit traits from other objects through prototype inheritance. Prototype inheritance is a simple object inheritance model most widely known for its use in JavaScript and Lua. These Object
s behave similarly to JavaScript's Object
s, but with the benefits of strict inferred typing and immutability when desired.
Object-specific methods can be inherited, overridden, and extended. Objects can also be tagged with optional types, allowing multiple dispatch to implement polymorphism.
Composing objects on an ad-hoc basis, instead of architecting classes and hierarchies up-front, can be a very efficient workflow: especially when experimenting on a new project, or when sculpting and customizing complex objects.
It's also just fun. Nobody likes ERROR: invalid redefinition of constant MyStruct
. (ikik revise.jl)
# constructing from scratch
Object{[TypeTag]}([StorageType] [; kwargs...])
# changing type ("converting")
Object{[TypeTag]}([StorageType,] obj::Object)
Object{[TypeTag]}([StorageType,] obj::Any)
Object{[TypeTag]}([StorageType,] props::AbstractDict)
# constructing from template
(template::Object)([; props...])
# prototype inheritance
Object{[TypeTag]}([StorageType,] (proto::Object,) [; kwargs...])
Syntax:
Object{[TypeTag]}([StorageType] [; kwargs...])
Easy.
obj = Object(x=1, y=2) # default type is `Mutable`
obj.x + obj.y # 3
obj.x = 2
obj.x + obj.y # 4
Can also access with []
syntax:
@show obj.x
@show obj[:x]
@show obj["x"]
As easy as JSON.
obj = Object(
a = [1,2,3],
b = Object(
c = "Hello!",
d = Dict(string(k)=>Char(k) for k = 1:255)
)
)
@show obj.b.c # "Hello!"
Iterable collections should be splatted into keyword arguments. Later arguments override earlier ones.
Remember to put ;
before the splatted object to ensure it splats into keyword arguments, not regular arguments. (note 1)
Named Tuples:
defaults = (a=420, b=3.14)
obj = Object(; defaults..., b=69) # nice
Notice that not only are values overridden by later arguments, but data types are overridden too.
Dictionaries:
obj = Object(; a=2, Dict(:a=>1, :b=>2)..., b=3)
Notice that later arguments, including those splatted in, always override earlier ones.
Generators:
messages = Object(; ((Symbol(name) => "Hello, $name") for name ∈ ["Joe", "Sally", "Mark"])..., Mark="G'day, Mark")
@show messages.Mark
Other Object
s:
obj = Object(a=1, b=2)
newObj = Object(; obj...)
@show (; obj...) == (; newObj...) # true
@show obj == newObj # false
@show Dict(obj...) # splat `obj` into a dictionary (as regular args, not keyword)
note 1: Splatting into keyword arguments keeps the names as part of the argument type, allowing the function to specialize on type and be type-stable. To see what I mean, run this:
((ar...; kwar...)->(ar, kwar))(:a=>1, :b=>2; c=3, d=4) .|> x->println(typeof(x))
and look for :c
and :d
. As a result, I only allow splatting into the keyword arguments of Object
. This is unlike Dict
s, which remain type stable even if the key names change; the type of an Object
should change if its property names change so that methods can specialize on the object type.
Syntax:
Object{[TypeTag]}([StorageType,] obj::Any)
If its properties are accessible with .
dot syntax, then it can be Object
ified.
struct Test1 a; b end
test1 = Test1('🐢', "Hello")
obj1 = Object(test1)
Note that obj1
is a copy; changes to an Object
ified mutable struct won't be reflected:
mutable struct Test2 a; b end
test2 = Test2('🐢', "Hola")
obj2 = Object(test2)
test2.a = '🐇'
obj2.a ≠ test2.a
Object
ifying arbitrary composite types is not recursive, for hopefully obvious reasons.
As we see later in the section on iteration, it's easy to splat the Object
back into the original constructor with Test1(obj1...)
.
Syntax:
Object{[TypeTag]}([StorageType,] props::AbstractDict)
If a dictionary is not splatted, then it will be assumed that it is being used to hold a hierarchical structure with nested dictionaries. A new Object
will be created recursively, with each nested dictionary being represented as a nested Object
.
using TOML
config = Object(TOML.parsefile("config.toml"))
The inverse recursive operation can be performed with
convert(Dict, config)
Note that splatting is not recursive; these are special methods for converting between nested dictionaries and Object
s.
Of course, once something has been converted into an Object
, it can be splatted to create new objects.
composed_config = Object(; config..., username="Why didn't I have a username before?")
You can also select specific items by indexing (this creates a new Object
with these properties):
config[(:usertype, :password)]
and using generators:
obj = Object(a=1, b=2, c=3, d=4, e=5)
obj[(k for k ∈ keys(obj) if obj[k] > 2)]
and you can drop specific items (this creates a new Object
with the same prototype, dropping only "own" properties when available; no error if dropping a property which doesn't exist)
drop(config, :creationdate, :userage)
We already saw splatting
(; obj...)
This splats into a NamedTuple, good for splatting into keyword arguments of functions.
You can also splat into a tuple of values:
(obj...,)
This is good for splatting into constructors; for example, if you've created an Object
based off a struct
, you can do this:
struct MyStruct{T} a::T; b::T, c::T end
x = MyStruct(1, 2, 3) # immutable.
y = Object(x) # mutable!
for k ∈ keys(y) y[k]^=2 end # let's square things up a bit
MyStruct(y...) # and we're back! (now it can be passed to methods specialized on MyStruct)
(bug note: currently, doing this with Dynamic
objects is NOT a good idea, because their keys are out of order.)
You can check the order of splatting by inspecting keys(y)
.
You can also loop by key and value
for (k,v) ∈ zip(keys(obj), values(obj))
println("key $k corresponds to value $v")
end
Object
s can be destructured like any other object with properties:
obj = Object(x=1, y=2, z=3)
let (; x, y) = obj
@show x + y
end
Three subtypes of object are provided by storage technique: Dynamic
, Static
, and Mutable
. Their external behaviors are identical, except for property-setting flexibility and performance.
- Dynamic: maximum flexibility—properties can be added or changed at any time, arbitrarily.
- Static: maximum performance—after construction, properties cannot be changed.
- Mutable: happy medium—properties can be changed at any time, but their types cannot change and new ones cannot be added.
If left unspecified, the default is Mutable
.
For Mutable
objects, you can't change property types or add new ones after object construction:
mut = Object(x=1, y=2) # Mutable is default
mut.x = 3.14 # error
mut.x = "hello" # error
mut.z = 42 # error
Add type argument of Static
, Mutable
, or Dynamic
to specify object type.
mut == Object(Mutable, x=1, y=2)# true
dyn = Object(Dynamic, x=1, y=2) # can change anything at any time
dyn.z = 3
dyn.x + dyn.y + dyn.z # 6
stc = Object(Static, x=1, y=2) # can't change anything after creation
stc.x = 2 # error
Dynamic
is very easy and casual to use, but unfortunately type instability causes lower performance. Good for hacking and playing, throwing stuff together before worrying about formalizing, but not so good for efficient runtime.
Dynamic
flexibility can also be dangerous when incorporated into large complicated inheritance structures; once things begin settling down and getting sorted out, start moving structures to Mutable
or Static
, or create composite types with struct
.
Syntax:
Object{[TypeTag]}([StorageType,] obj::Object)
Keep same property values and prototype, but change the object type between Dynamic
, Mutable
, or Static
.
obj = Object(Mutable, a=1, b=2) # `Mutable` is the default, chosen explicitly here
dyno = Object(Dynamic, obj) # Create `Dynamic` from `Mutable`
dyno.c = 3
locked = Object(Static, dyno) # Create `Static` from `Dynamic`
An Object
can have member-specific methods:
obj = Object(Dynamic, a=2)
computefunc = function(self, b) self.a * b end
obj.compute = computefunc
@show obj.compute(3) # 6
Calling obj.compute
passes obj
in as the first argument.
Implementation-wise, accessing obj.compute
yields a closure which captures obj
and passes it as the first argument to computefunc
.
Use the let
keyword to create local scope to define the flavors of a function, and make it a property of the object. Outside that local scope, the name given to the polymorphic function is invalid.
obj = Object(;
a=1,
b=2,
func = let
function f(self) self.a + self.b end
function f(self, x::Int) self.a + x end
function f(self, x::Float64) x end
end
)
@show obj.func() # 3
@show obj.func(5) # 6
@show obj.func(2.5) # 2.5
Of course, you can make the method have global scope too, if desired.
If it's desired for an object to store a function for retrieval, then store a reference to it with Ref
and access it with dereferencing syntax []
:
obj = Object(storedfunc = Ref(x -> x^2))
f = obj.storedfunc[] # retrieves the function as-is
@show f(5) # 25
Note
Because every function has a different type signature, even for Mutable
objects you cannot mutate member methods. And because Ref
also carries the referenced object's type signature, you can't mutate references to functions either.
obj.storedfunc[] = x -> x^3 # error
To change some functions but keep the other properties and methods, either use splatting or inheritance to construct a wholly new object, or use a Dynamic
object type (like the example with computefunc
above).
Ultimately, the role that a struct
serves is to define a specific structure for how data is to be organized, and to make sure both human and compiler know it—not just for code consistency, but so that specialized methods can be generated for runtime speed. With Object
s, that role is served by templates.
Syntax:
(template::Object)([; props...])
Every Object
instance is a functor, and calling it uses it as a template to create a new object. Example:
Template = Object(a=0, b=0.0)
obj = Template(a=2)
@show ownpropertynames(obj)
The newly constructed object has exactly the same type, and the same property names and property types, as the template; any properties that are not specified assume default values as specified by the template. Attempting to add properties or change their type will result in errors.
For example:
obj = Template(b=1)
@show obj.b # 1.0, not 1
Template(a=2.5) # error, can't convert to Int
Template(c=1) # should throw an error but currently just ignores extra argument
For comparison, composition by splatting:
Object(; Template..., a=2.5) # ok
So notice that unlike the other construction techniques, using a template is much more restrictive. Unlike composition techniques, it forces the properties to have exactly the same types as the template.
See bottom for some benchmarking.
Once you've composed a template, you can use it for type assertions to ensure functions are receiving that specific data structure. The type of any Object
encodes all the available information regarding the property types and their ordering, so a type assertion can be performed like so:
function myfunc(a::typeof(Template))
# do stuff...
end
Syntax:
Object{[TypeTag]}([StorageType,] (proto::Object,) [; kwargs...])
Create an object using proto
as a prototype. The new object defaults to the same type as its prototype, unless otherwise specified. Think of it like the prototype is picking up new tricks and being repackaged into a new object.
obj = Object(a=1, b=2) # original object
@show obj.a, obj.b # (1, 2)
newObj = Object((obj,), b=3, c=4) # newObj inherits a and b, and overrides b
@show newObj.a, newObj.b, newObj.c # (1, 3, 4)
obj.a, obj.b = 2, 1 # change in obj.a passes through to newObj, obj.b does not
@show newObj.a, newObj.b, newObj.c # (2, 3, 4)
newNewObj = Object((newObj,), c=5, d=6)
@show [newNewObj[s] for s ∈ (:a,:b,:c,:d)] # [2, 3, 5, 6]
Notice that proto
is packed into a one-sized Tuple
. Remember to add the comma!
Implementation-wise, newObj
stores a reference to its prototype obj
; all properties and methods of obj
are accessible to newObj
, and any changes to obj
will be reflected by newObj
. newNewObj
stores a reference to newObj
.
Note that because these Object
s are the default Mutable
, any properties not specified as "own" properties cannot be changed. This means that newObj.a
cannot be changed, since it was never declared as its own property, and it will always reflect obj.a
. To make arbitrary changes use Dynamic
objects instead, and to lock obj
from changing use a Static
object instead.
Because prototypes are inherited by storing a reference, it is possible to build inheritance chains where traits are replicated and pass through many inheriting objects.
You can make an object which is somewhat static, and somewhat mutable, using inheritance:
a = Object(Static, a=1)
b = Object(Mutable, (a,), b=2)
c = Object(Static, (b,), c=3)
@show (; c...)
Object c
has three accessible properties: c.a
, c.b
, and c.c
. Among these, only c.b
is mutable by changing b.b
.
b.b = 0
@show (; c...)
This can get really funky if you use a Dynamic
object as a prototype for a Static
or Mutable
object. Any properties that the static or mutable child object owns, of course, will be type-stable. However, any properties inherited from the dynamic prototype will be type-unstable. For example:
a = Object(Dynamic; a=1)
b = Object(Static, (a,); b=2)
@code_warntype (x->x.b)(b)
@code_warntype (x->x.a)(b)
Understand this, and master the universe.
Strictly speaking, multiple inheritance isn't implemented. But it's easy to splat objects together to compose a new object that takes traits from multiple objects. This is more than likely good enough for just about anything.
parent = Object(firstname="Jeanette", lastname="Smith", hobby="Fishing")
friend = Object(hobby="Skiing")
child = Object((parent,); friend..., firstname="Kevin")
@show child.firstname, child.lastname, child.hobby
# from self, inherited from parent, and adopted from friend
Prototype inheritance comes primarily from parent, but friend's preferences compose by getting splatted in and override parent's.
Changes in parent.lastname
are reflected in child
, but changes in friend.hobby
are not.
To create a new independent object with the same properties but breaking the inheritance chain, splat the object:
libertine = Object(; child...) # free Kevin from Jeanette
Try this:
a = Object(i=1, j=2);
b = Object(j=3, k=4);
c = Object(k=5, l=6);
d = Object(l=7, m=8);
e = Object(m=9, n=10);
@show x = Object((Object((Object((Object((a,); b...),); c...),); d...),); e...)
@show y = Object(; a..., b..., c..., d..., e...) # objects splatted later override earlier objects
@show z = Object(; x...)
@show Dict(x) == Dict(y)
Typically, you want objects of a certain class to share common behaviors and perhaps some set of properties, and to have a remaining set of personalizable traits that are instance-specific and possibly mutable.
The shared behaviors can obviously be included using a prototype.
As for the traits, defaults can be splatted in as a template, and then further overridden on an instance-by-instance basis. They can serve as placeholders, setting a) which traits will be personalized, b) reasonable defaults, and c) what datatypes they will hold. Example:
Person = Object(
arms=2,
legs=2,
talk=function(self) self.age > 5 ? :loud : :quiet end,
traits = Object(Static;
age=0, # years
height=0.0, # centimeters
siblings=0, #
name=""
)
)
amy = Object((Person,); Person.traits..., name="Amy") # Amy hasn't been born yet and currently only has a name
amy.height = 45.5; # Amy has now been born, but is still zero years old
@show amy
@show amy.talk()
The type of Person.traits
doesn't matter here because it's just splatted in.
Notice how Person.traits
serves as a placeholder to set default personal values and their types. This allows Mutable
instances to have their personalizable traits updated after construction, and Static
instances can have default values for traits that might otherwise be left unspecified.
Additional note when using Mutable
and Static
types: the resulting object type matters because when the object is passed to a function, the function is compiled to that type. When using the same prototype and default traits, the object type is fully consistent (even down to the argument ordering!). This means that functions that have been compiled for one instance, don't have to be recompiled for additional instances.
joe = Object((Person,); Person.traits..., name="Joe", age=45, siblings=3)
@show joe.talk()
@show typeof(joe)
@show typeof(joe) == typeof(amy)
a note
After a mutable Object
has been constructed, its property types must remain consistent. However, when splatting an object, its types can be overridden by subsequent arguments. For example:
joe = Object((Person,); Person.traits..., name="Joe", age=45.5, siblings=3)
If joe
's age
is set to 45.5
, then it overrides what was by default an Int
with a floating point number, and joe
's type is no longer the same as amy
's. If this can be a problem, use a template instead of splatting (as described above).
use ur imaginatino oy
We've covered the three methods to copy traits: by splatting, by template, and by prototype inheritance.
For freehand composition, crafting new objects out of existing ones by copying and pasting their features, splatting is great.
newObj = Object(; obj1..., obj2..., a=1, b=2)
If an object is going to be duplicated many times, it can be considered to be a member of some sort of class of similar objects. In this case, either a struct
can be defined, or the object can be used for repeated construction as a template.
duplicate = obj(a=1, b=2)
If many child objects will incorporate some features of a parent object, but they will simultaneously all have the same value (and so replicating the value isn't desirable), this is handled by prototype inheritance.
child = Object((parent,); a=1, b=2)
Most of the time, you'll just want to compose simple objects by splatting and then replicate them as templates. But occasionally you'll find inheritance useful.
Object
s have an optional type tag which doesn't affect Object
behavior per se, but allows methods to specialize on multiple dispatch. This tag can be a Type
, a Symbol
, a Tuple
, a number... anything for which isbits
evaluates to true. For example:
a = Object{:pos}(x=5)
b = Object{:neg}(x=5)
g(obj::Object{:pos}) = obj.x^2
g(obj::Object{:neg}) = -obj.x^2
@show g(a), g(b) # (25, -25)
This type tag is automatically inherited by template or by prototype.
@show g(a(x=2)), g(b(x=2)) # (4, -4); template
@show g(Object((a,),x=2)), g(Object((b,),x=2)) # (4, -4); prototype
The type tag can also be changed.
To change type while inheriting traits (i.e., using a
as a prototype):
a_neg = Object{:neg}((a,))
To change type while copying traits (i.e., copying a
's properties and keeping a
's prototype):
a_neg = Object{:neg}(a)
To change type while flattening traits (i.e., copying a
's properties and any from a
's prototype):
a_neg = Object{:neg}(; a...)
(note this is a bad example because a
has no prototype)
When unspecified, the type tag default is Nothing
.
Methods can behave differently depending on the tag type of their caller:
traits = Object(age=0, name="", punish = let
function f(self::Object{:child}) "stand in corner for $(self.age) minutes" end
function f(self::Object{:teen}) "scold sternly for $(self.age) seconds" end
function f(self::Object{:adult}) "express disappointment for $(self.age) years" end
end)
tommy = Object{:child}((traits,); name="tommy", age=5)
jeff = Object{:adult}((traits,); name="jeff", age=25)
@show tommy.punish(), jeff.punish()
Type tags, when types are used, can be given hierarchy.
# type hierarchy
abstract type Animal end
abstract type Human <: Animal end
abstract type Dog <: Animal end
# prototypes
const Prototypes = Object(Dynamic)
Prototypes.Animal = Object{Animal}(; eyes=2, legs=4, size=:large)
Prototypes.Human = Object{Human}((Prototypes.Animal,); legs=2, artificial_legs=0, size=:medium)
Prototypes.Dog = Object{Dog}((Prototypes.Animal,); size=:small)
# defining a method extends naturally to subtypes
getspeed(animal::Object{<:Animal}) = animal.legs
# defining a method for the human subtype
getspeed(person::Object{<:Human}) = person.legs + person.artificial_legs
let (; Animal, Human, Dog) = Prototypes
sparky = Dog()
@show getspeed(sparky) # 4
joe = Human(legs=1); # lost in a tragic automobile accident
@show getspeed(joe) # 1
joe.artificial_legs = 1 # modern technology
@show getspeed(joe) # 2
end
Notice that the path for objects to inherit traits (prototype inheritance) is separate from the path of obtaining type hierarchy (type tagging), so there's flexibility for an object to adopt traits from any other type. It's like a coal miner learning how to code, if that's even possible.
getprototype(obj::Object)::Union{Object, Nothing}
Gets obj
's prototype object.
Unlike JavaScript, an object's prototype cannot be changed (so there's no setprototype!
function).
ownpropertynames(obj::Object)
ownproperties(obj::Object)
drop(obj::Object, n::Symbol...)
Obviously Static
will be fastest at runtime and Dynamic
slowest. Because accessing elements from a Dynamic
object is type-unstable, calling functions on their values can be slow.
If using Dynamic
objects is desired anyway, their performance can be helped by adding type assertions whenever their values are being fed into performance-critical functions.
obj = Object(Dynamic, a=1, b=2)
f(x) = x.a + 1
@code_warntype f(obj) # output is type-unstable
g(x) = x.a::Int + 1
@code_warntype g(obj) # output is type-stable
When accessing a member method, a closure is returned which captures the object and passes it as a first argument:
obj = Object(a=1, b=2, h = this -> this.a + this.b)
somevar = obj.h
@show somevar() # 3
Same when accessing it by index, or when destructuring an object:
@show obj[:h]() # 3
(; h) = obj
@show h() # 3
But when iterating over the object, such as by splatting or looping by (key,value), the original function is given (because we want to be able to splat the function into new objects as a member method)
Dict(obj...)[:h]() # error
@show Dict(obj...)[:h](obj) # 3
The type of an Object
's properties, and how they are stored, is encoded in its second type parameter. For example, try this:
@show typeof(Object(Dynamic; a=1, b=2))
@show typeof(Object(Static; a=1, b=2))
@show typeof(Object(Mutable; a=1, b=2))
If you want to test for objects with specific properties, but disregarding the tag type, you can do something like this:
obj1 = Object{1}(a=1, b=2)
obj2 = Object{2}(a=2, b=1)
@show obj2 isa typeof(obj1) # false
T = Object{<:Any, typeof(obj1).parameters[2]}
@show obj2 isa T # true
This will check that obj1
and obj2
have exactly the same arguments in the same order, and are both Mutable
. Here's something that just checks to see if an object is Dynamic
or not:
obj2 isa Object{<:Any, <:Dynamic} # false
This allows you to make different implementations depending on how the Object
is represented internally. I don't know why you would want to, but you can.
How can you filter for Object
s that store a specific set of parameter names, or with specific types, but disregarding their order or whether they're Dynamic
or Static
? It's possible, but unless the type language becomes even more expressive than it already is, is probably a waste of time.
Static objects:
using BenchmarkTools
struct TestStatic a::Float64; b::Int; c::Char end
Template = Object(Static, a=0.0, b=0, c='a')
# constructing by splatting: 14.915 ns (0 allocations: 0 bytes)
@btime Object(Static; $Template..., a=rand(), b=rand(1:10), c=rand('a':'z'));
# template construction: 14.900 ns (0 allocations: 0 bytes)
@btime $Template(a=rand(), b=rand(1:10), c=rand('a':'z'));
# construction from scratch: 14.830 ns (0 allocations: 0 bytes)
@btime Object(Static; a=rand(), b=rand(1:10), c=rand('a':'z'));
# basic struct: 14.915 ns (0 allocations: 0 bytes)
@btime TestStatic(rand(), rand(1:10), rand('a':'z'));
Mutable objects have almost identical performance:
mutable struct TestMutable a::Float64; b::Int; c::Char end
Template = Object(Mutable, a=0.0, b=0, c='a')
# constructing by splatting: 21.063 ns (3 allocations: 48 bytes)
@btime Object(Mutable; $Template..., a=rand(), b=rand(1:10), c=rand('a':'z'));
# template construction: 21.565 ns (3 allocations: 48 bytes)
@btime $Template(a=rand(), b=rand(1:10), c=rand('a':'z'));
# construction from scratch: 21.063 ns (3 allocations: 48 bytes)
@btime Object(Mutable; a=rand(), b=rand(1:10), c=rand('a':'z'));
# basic struct: 16.834 ns (1 allocation: 32 bytes)
@btime TestMutable(rand(), rand(1:10), rand('a':'z'));
The performance when constructing from a template is maintained even if the arguments are out of order from their original creation (try it).
Using object templates as a stand-in for a struct
is nice because templates provide default values, and you can fill in properties out of order from how they were originally defined.
The drawback is that, if a property would've had a Union
or UnionAll
type, this can be communicated in a struct
definition but it cannot be communicated (or even allowed!) by the template. For structs with multiple variables of variant types, the number of templates required to communicate all the possibilities could be restrictive.
This can be circumvented with object splatting, where property types can be overridden by new arguments. However, there is no enforcement of the types.
So for some things, it seems there's no way around using struct
.
Unless... maybe I can think of something? NamedTuples
provide the ability to carry abstract types...
Object{UT}(OT, (a=Number, b=Float64, c=Any, d=String); a=1, b=2, c=3, d="hi") 🤔
Can make it optional to specify types, and values. Any type that's not specified is assumed to be the concrete type of the given value; any value not specified takes on undef
. And any value that disagrees with the given type causes an error.
Can possibly do the same thing when throwing an arbitrary object into Object
; dump
its constructor and inspect the types of its properties.
HOLY FURK
Not only can you make NamedTuple
s have entries with abstract values; you can do the same thing with Ref
too!
x = NamedTuple{(:a,:b), Tuple{Any,Any}}((Ref{Any}(1),Ref{Any}(2)))
x.a[] = "what"
x.a[] = x->x^2
x.a[] = 3.14
BOOM
Possible...
Template = Object(MyStruct[, template::MyStruct][; kwargs...])
^ take all fields of MyStruct, and use that to create a template ObjConstructor
which adopts the same types including abstract types and parametric types
Template = Object((a=Int, b=Number)[; kwargs...])
Template = Object(NamedTuple{NTuple, Tuple{<:Type}}(Tuple)[; kwargs...])
How to mimic parametric types? e.g. struct MyStruct{T} a::T; b::T end
Construct objects with abstract types: obj = Template(...)
; then to convert to concrete types, splat Object(; obj...)
Is there a way to implement inner constructors, and to initialize undef properties? This would be very useful for properties that are created by method calls... otherwise I need to use Dynamic
which sucks.
Also: add numbered access to Objects. It's sometimes useful when args splatting. And definitely use ordered dicts.
zr