So for fun I started to write a simplistic fantasy text RPG. One of the things included is the notion of various items a character can randomly find, such as treasures, weapons, and armor. I thought that it would be useful to randomly create an item (here a weapon) in the following manner:
type
Item* = ref object of RootObj
description*: string
Treasure* = ref object of Item
baseValue*: int
Weapon* = ref object of Treasure
baseDamage*: int
Sword* = ref object of Weapon
Axe* = ref object of Weapon
Wand* = ref object of Weapon
Daggers* = ref object of Weapon
type
Damaging {.explain.} = concept w
w.baseDamage is int
proc newSword(): Sword =
discard
proc newAxe(): Axe =
discard
proc newWand(): Wand =
discard
proc newDaggers(): Daggers =
discard
const Weapons: seq[proc():Damaging] = @[newSword, newAxe, newWand, newDaggers]
proc genWeapon(): Weapon =
Weapons[random(Weapons.len)]()
Unfortunately, using concepts in this way doesn't seem to work. Is there some approach I could take to achieve this?
Best regards,
Steve
You can't store concepts in sequences, they work like generics. Instead, without too many changes you could make it work like this:
type
Damaging = proc: Weapon {.nimcall.}
const Weapons: seq[Damaging] = @[
proc: Weapon = Sword(),
proc: Weapon = Axe(),
proc: Weapon = Wand(),
proc: Weapon = Daggers(),
]
You can't store concepts in sequences
Doesn't stevos code try to store just addresses of procs which return a Concept? Why shouldn't that be possible? The size of Damage doesn't have to be known for that, so it should be ok that it's not a concrete type.
The actual error is:
Error: type mismatch: got (proc (): Axe{.noSideEffect, gcsafe, locks: 0.}) but expected 'proc (): Sword{.noSideEffect, gcsafe, locks: 0.}'
It seems to come from seq initialization: the type expected for all elements is set by the exact type of the first element and not by the actual type declaration. Is that correct/necessary behavior?
Thank you, this solution worked! It is a little more overhead than I was hoping for (ultimately I'm going to use the material in this simplistic game as an intro to Nim for other programmers) but this does exactly what I wanted.
Thanks again for your help,
Stevo
Doesn't stevos code try to store just addresses of procs which return a Concept? Why shouldn't that be possible? The size of Damage doesn't have to be known for that, so it should be ok that it's not a concrete type.
I thought this too, and sure enough you can define procs that return concepts:
type
Conc = concept c
c.v is int
MyObj = object
v: int
proc test: Conc =
result = MyObj(v: 1)
echo test().repr
This outputs [v = 1].
However adding this line:
var x = [test]
Crashes the compiler with:
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
In fact, looks like declaring a seq of concept crashes:
var x: seq[Conc] # crash
So looks like this is a bug, as you'd expect an error rather than a crash if it wasn't allowed.
imagine we have an ordinary generic proc:
proc genericProc[T](a: T): T =
discard
and this generic proc might be instantiated with different types.
If we want to get the proc's address, we need to tell the compiler which one of those proc instances using generic specialization:
var s = [genericProc[int]] #get the genericProc's address with `int` instance and put it into array
proc definition contains concept is another form of generic proc. If we want to get the proc's address, there should be a mechanism to disambiguate which instance that we need. But unfortunately, looks like there is nothing like concept specialization.
the first issue:
var x = [proc_with_concept_address]
need some addition in the language specification describing concept specialization.
the second issue:
var y = [some_concept]
perhaps this can be realized if the compiler put some restriction or constraint on the types involved.
both issues need to be reported to nim's bugs tracker, perhaps someone interested to fix it in the future.
@coffeepot Same crash with this:
proc a[T](b: T): string =
$b
var s = @[a]
Seems to be a general problem with non-concrete types (all type classes?).Of course the compiler should not crash, but other than that I think there is no issue at all.
Generic (non-concrete types) exist only as a compile time abstraction. Each value that exists at runtime actually has a concrete type. You cannot put anything which does not have a concrete type inside a container, because it is not a value. It is something that simply does not exist at runtime
@andrea Right. And even for a proc returning a non-concrete type (the original question), calling code would have to know the size of the thing it gets back, so that cannot work.
As I understand it now:
type
C = concept c
c.num is int
Cvt = vtref C # a VTable type, not yet implemented
A = object of RootObj
name: string
num: int
B = object of RootObj
rate: float
num: int
var
a = A(name: "me", num: 1)
b = B(rate: 3.2, num: 2)
s1: seq[RootObj] = @[a, b] # this works, type erasure by base type
s2: seq[C] = @[a, b] # this cannot work because C isn't concrete: e.g.
# the size of a C is unknown
s3: seq[Cvt] = @[a, b] # this should work when VTable types are here
Oh, gosh. I finally understand why Concepts are currently useless in Nim.
I want to write a graph library, like this:
proc shortest_path(g: Graph): seq[Graph.Node]
If Graph were a Concept, I could specify it like this:
GGraph* = concept g
type Node = type(g.none())
In other words, I want "Node" to be "Graph.Node" somehow. That way, I can always infer the Node type from the Graph type.
But concepts cannot be used in standard Nim containers! That's the problem. I actually don't want a vtable. I want a concrete container of a concrete type, to be filled in at compile-time.
So I end up with something like this:
proc shortest_path[Graph, Node](g: Graph): seq[Node]
That would work fine, but it's verbose. That's why I want to use Concepts instead of Generics.
In order to avoid increasingly long generic parameter lists, I think I'm forced to use Nim "templates".
template graphlib(Graph, Node, Weight: typedesc) =
proc add_edge(g: ref Graph, u, v: Node, weight: Weight) =
...
# etc
proc shortest_path(g: ref Graph): seq[Node] =
....
graphlib(MyGraph[MyNode, MyWeight], MyNode, MyWeight)
Disclaimer: changed my nick, Lando was taken on github (big surprise..).
Oh, gosh. I finally understand why Concepts are currently useless in Nim.
Wouldn't call them useless, they serve a purpose as compile time type classes, just as generics do.
But concepts cannot be used in standard Nim containers! That's the problem.
Yes, and AFAIK we cannot do anything about it. A language with strong static typing needs to know what actual runtime type a container element will have at some point of the compilation process.
I want a concrete container of a concrete type, to be filled in at compile-time.
Can't help you with that, but if that requirement could be relaxed (a seq[int] with node indices can represent a graph path, e.g.), concepts let you separate generic from type-specific code with hardly any generic call parameters:
type
Node = concept n
metrics(n, n) is int
# Node could be named differently here
Graph[Node] = concept g
g.nodes is seq[Node]
g.addIt(Node)
Station = ref object
RailroadNet = ref object
stations: seq[Station]
proc nodes(r: RailroadNet): seq[Station] = r.stations
proc addIt(r: RailroadNet, s: Station) =
r.stations.add s
proc metrics(a, b: Station): int = 5
proc add(g: Graph, n: Node) =
# generic stuff here
g.addIt n # calls type specific stuff
proc shortest_path(g: Graph): seq[int] =
# generic stuff here
# calls type specific stuff:
result = if metrics(g.nodes[0], g.nodes[1]) > 2: @[0, 1] else: @[1, 0]
let r = RailroadNet(stations: @[Station()])
r.add(Station())
echo shortest_path(r)
Note: generic concepts still give you crashes / crazy looping instead of a decent error if you do illegal things.@cdunn2001 I'm not sure why you think it doesn't work. I typed up your example and the following works (I'm using Nim 0.18.1):
type GraphConcept* = concept g
type NodeType = type(g.node())
type Node* = object
n*: int
type Graph* = object
nodes*: seq[Node]
proc node*(g: Graph): Node =
Node()
proc shortest_path*(g: GraphConcept): seq[g.NodeType] =
g.nodes
let nodes = @[Node(n:1), Node(n: 2)]
let g = Graph(nodes: nodes)
echo shortest_path(g)
Wow. I don't know now.
Slightly modifying your example, here is closer to what I want:
type
NodeConcept* = concept n
$n is string
GraphConcept*[NodeConcept] = concept g
type NodeType = NodeConcept #type(g.node())
g.nodes() is seq[NodeType]
Node* = object
n*: int
Graph* = object
mynodes*: seq[Node]
proc nodes*(g: Graph): seq[Node] =
g.mynodes
#proc node*(g: Graph): Node =
# Node()
proc shortest_path*(g: GraphConcept): seq[g.NodeType] =
g.nodes
let nodes2 = @[Node(n:1), Node(n: 2)]
let g = Graph(mynodes: nodes2)
echo shortest_path(g)
And that works too!
So I'm confused, but happy.
Oddly, if I change part to
NodeConcept* = concept n
$n is string
n.foo() is string
Then it still works. But why? n.foo() does not exist. I thought these were constraints. And in fact, I can call any method on the GraphConcept that actually exists on the actual Graph, even if I do not specify it in the "concept", e.g.
NodeConcept* = concept n
$n is string
...
proc bar(g: Graph): string =
"bar()"
proc shortest_path*(g: GraphConcept): seq[g.NodeType] =
echo g.bar()
g.nodes
So I get the impression that "concept" has been weakened, not longer for type-constraints but only for auto-creation of generics. That's fine, but it's not what I expected.I think you may be confused about the following:
type
NodeConcept* = concept n # LINE A
$n is string
GraphConcept*[NodeConcept] = concept g # LINE B
type NodeType = NodeConcept
g.nodes() is seq[NodeType]
The NodeConcept on line B is actually not the same as the NodeConcept on line A. It's just another name for a type parameter.
The following works:
type
NodeConcept* = concept n
$n is string
GraphConcept*[N] = concept g
type NodeType = N
g.nodes() is seq[NodeType]
Graph*[N] = object
mynodes*: seq[N]
Node* = object
n*: int
proc nodes*[N](g: Graph[N]): seq[N] =
g.mynodes
proc shortest_path*[N](g: GraphConcept[N]): seq[g.NodeType] =
g.nodes
proc `$`(n: Node): string = $n.n
let nodes2 = @[Node(n:1), Node(n: 2)]
let g = Graph[Node](mynodes: nodes2)
echo shortest_path(g)
And it also fails to compile if you remove the $ proc for Node, which is what I think you were hoping would happen.
Was "concept" changed recently to work this way? > If not, sorry for my confusion. If so, I definitely like this better!
AFAIK, concepts haven't changed much (in semantics) in at least the past ~1.5 years, maybe longer, though the implementation continues to evolve (fixing bugs, etc.).
(I'm not an authority on concepts, just a user who's been kicking the tires for some time.)