I think I read the term "Composition over inheritance" somewhere, so I asked myself if Nim can support anonymous structs/objects/tuples?
The (my) goal is to avoid expressions like a.b.x = 10.0 in composed structures. C11 and Golang seems to support that.
https://en.wikipedia.org/wiki/Composition_over_inheritance
http://golangtutorials.blogspot.de/2011/06/anonymous-fields-in-structs-like-object.html
http://modelingwithdata.org/arch/00000113.htm
and the related Golang discussion:
http://spf13.com/post/is-go-object-oriented/
https://www.reddit.com/r/golang/comments/27p2bc/is_go_an_object_oriented_language_spf13com/
I like the idea. You can get closer using converters https://glot.io/snippets/efrzovlcts
I think that, for complex structures, you may prefer to 'unpack' it as your example. A step forward would be to allow dynamic composition https://www.wikiwand.com/en/Entity_component_system
@Stefan_Salewski I was curious about this, and @Varriount gave a good idea about overloading the . procedure, so I created his described solution. This macro has the ability to operate on all object types, but you can easily change the obj: typed argument to be only specific to your wanted type if there are strange collisions with the built-in dot procedure.
Here it is, fully commented and hopefully use-able and understandable:
import macros
type
DotField = object
parents: seq[NimNode]
symbol: NimNode
iterator findSyms(obj: NimNode): NimNode =
## Iterate recursively through the symbols
## attached to the object using a stack
var stack: seq[NimNode] = @[]
stack.add(obj)
while stack.len() != 0:
let n = stack.pop()
if n.kind == nnkSym:
yield n
else:
for c in n.children:
stack.insert(c, 0)
proc findFields(obj: NimNode): seq[NimNode] {. compileTime .} =
# ObjectTy
# Empty
# RecList
# Sym "name"
# Sym "surname"
# Sym "age"
result = @[]
var recList: NimNode
var tp = obj.getType()
if tp.kind == nnkBracketExpr:
# This is a ref object:
#
# BracketExpr
# Sym "ref"
# Sym "A:ObjectType" <- getType on this node
#
# A:ObjectType <- then getType on this to get the actual A object type
#
tp = obj.getType()[1].getType()
# nnkRecList is the "Record List" or field list of the object
recList = tp.findChild(it.kind == nnkRecList)
if recList.kind != nnkNilLit:
for sym in recList.findSyms():
result.add(sym)
proc isNil(dotField: DotField): bool =
## Check if our custom field object is nil
return dotField.parents.isNil and dotField.symbol.isNil
proc getNestedField(obj: NimNode, field: NimNode): DotField{.compileTime.} =
## Get the nested field iteratively using a stack
var stack: seq[DotField] = @[DotField(parents: @[], symbol: obj)]
var foundField: DotField
let fieldRep = $field
while stack.len() > 0 and foundField.isNil():
# Pop an obj off the stack
let dotField = stack[^1]
stack.delete(stack.len - 1)
var
parents = dotField.parents
currObj = dotField.symbol
# Put the current symbol in the parents
# of the next symbol
parents.add(currObj)
for sym in findFields(currObj):
let newDotField = DotField(parents: parents, symbol: sym)
if $sym.toStrLit() == fieldRep:
# We've found our field!
foundField = newDotField
break
else:
# Haven't found it yet, keep iterating
stack.add(newDotField)
return foundField
proc transformToDotExpr(foundField: DotField): NimNode =
## Transform the found field into a dot expression.
##
## DotField(parents: @[a, b], symbol: c)
##
## turns into:
##
## a.b.c
##
result = newNimNode(nnkDotExpr)
# We basically want to turn DotField into an expression,
# so iterate the parents and create a new dot expression
# every 2 parents
for i in 0 ..< foundField.parents.len():
if result.len() == 2:
result = newDotExpr(result, foundField.parents[i])
else:
result.add(foundField.parents[i])
# If the dot expression has 2 children, it means
# it's already a full dot expression, so create a new
# one with the last symbol as the field being accessed
if result.len() == 2:
result = newDotExpr(result, foundField.symbol)
else:
# otherwise, the dot expression has one free space,
# so just add the last symbol to it
result.add(foundField.symbol)
macro `.`*(obj: typed, field: untyped): untyped =
## The anonymous field macro. This allows an object of structure
## a.b.c.d to access all subfields in one `dot` call. To call `d`, this
## macro allows simply to reference it via `a.d`
##
## called as `a.d`
let foundField = getNestedField(obj, field)
if not isNil(foundField):
result = transformToDotExpr(foundField)
else:
# Get the original call
result = callsite()
if result.kind == nnkCall:
# If this is a proc or method call, change it
# to a proc call syntax because we don't want
# infinite recursion on the macro call
#
# objName.procName()
#
# Call
# Ident !"."
# Sym "objName" <- this is our object
# StrLit procName <- this is our proc
#
result = newCall(ident($result[2]), result[1])
And then you can use it just like this:
import anonymous_struct
type
C = object
y: int
B = object
x: int
c: C
A = ref object
b: B
var c = C(y: 0)
var b = B(x: 5, c: c)
var a = A(b: b)
echo a.x
echo a.y
# Prints:
# 5
# 0
Maybe I should put this in a repo :) You have permission to use this code however you want, but some attribution never hurts!
jyapayne, this is really interesting.
Would it be possible to create a macro to compose the fields from source objects to create a new object? Duplicate field name/type combinations would be ignored but duplicate names with different types would have to throw an exception or something.
If so, wouldn't it then be possible to determine if an object contains the fields of another at compile time with the same name and type?
type
A = ref object
x: int
B = object
y: int
C = object
z: int
D = object
a: int
E = compose(A, B, C)
var
e = E(x=1, y=2, z=3)
echo e.x, ",", e.y # 1,2
#echo e.a # error
echo C in e # true
echo D in e # false, because the int field name doesn't match
Aside from general composition work, this would be amazing for an ECS. I'm not sure how useful to an ECS being able to only determine the type at compile time would be. Perhaps the compose macro could automatically calculate an additional field that stores which source objects it was constructed from and then allow run time 'class' checking without any overhead of RTTI.
Nim seems ripe for a really fast ECS implementation thanks to it's metaprogramming! Would be amazing to have things like this as a gamedev language addons library.
Anyway, my macro skills are non-existent at the moment, but it'd be interesting to know how possible this is, any pitfalls or things to think about. I do think your macro, jyapayne, would be pretty useful though, just wondering if the redirection penalty could be removed.
@andrea Alright :) Done! https://github.com/jyapayne/subfield
@coffeepot
E = compose(A, B, C)
This exact syntax probably isn't possible, but you could probably do something like this:
compose(E, A, B, C)
Anyway, my macro skills are non-existent at the moment, but it'd be interesting to know how possible this is, any pitfalls or things to think about. I do think your macro, jyapayne, would be pretty useful though, just wondering if the redirection penalty could be removed.
I think it's definitely possible :) Just the syntax pitfall above I think is one of the only drawbacks.
What is this redirection penalty you speak of? :)