Concept redesign
Goals:
- Empower concepts to make generic code type-checkable at declaration time,
not only at instantiation time.
- Make concepts easier to understand and easier to implement.
- Provide an escape hatch in generic code so that e.g. adding a
debug echo or log statement does not require a far reaching type
constraint addition.
- Old code with unconstrainted generic parameters keeps to work.
- Do not base
concept's implementation on system.compiles which is
under-specified in Nim, very tied to the current implementation, and
finally, slow.
Non goals:
- Support every little detail that the old concept design supported. For this
we have the escape hatch plus the fact that you can leave your T
underspecified.
- Support accidentical features. ("Look! With this hack I can specify that the
proc needs a cdecl calling convention!")
- Support "cross parameter" constraints like "sizeof(a) == sizeof(b)". I have
yet to see convincing examples of these cases. In the worst case, we could
add enableif for this without bloating the concept's design.
- Turning concepts into "interfaces". This can already be accomplished with
macros. Having said that, since these concepts are declarative, they are
easier to process with tooling and arguably easier to process with macros
as well.
Atoms and containers
Concepts come in two forms: Atoms and containers. A container is a generic
concept like Iterable[T], an atom always lacks any kind of generic
parameter (as in Comparable).
Syntactically a concept consists of a list of proc and iterator
declarations. There are 3 syntatic additions:
Self is a builtin type within the concept's body stands for the
current concept.
each is used to introduce a generic parameter T within the
concept's body that is not listed within the concept's generic
parameter list.
either orelse is used to provide basic support for optional
procs within a concept.
We will see how these are used in the examples.
Atoms
type
Comparable = concept # no T, an atom
proc cmp(a, b: Self): int
ToStringable = concept
proc `$`(a: Self): string
Hashable = concept
proc hash(x: Self): int
proc `==`(x, y: Self): bool
Swapable = concept
proc swap(x, y: var Self)
Self stands for the currently defined concept itself. It is used to avoid
a recursion, proc cmp(a, b: Comparable): int is invalid.
Containers
A container has at least one generic parameter (most often called T). The
first syntactic usage of the generic parameter specifies how to infer and bind T.
Other usages of T are then checked to match what it was bound to.
type
Indexable[T] = concept # has a T, a collection
proc `[]`(a: Self; index: int): T # we need to describe how to infer 'T'
# and then we can use the 'T' and it must match:
proc `[]=`(a: var Self; index: int; value: T)
proc len(a: Self): int
Nothing interesting happens when we use multiple generic parameters:
type
Dictionary[K, V] = concept
proc `[]`(a: Self; key: K): V
proc `[]=`(a: var Self; key: K; value: V)
The usual ": Constraint" syntax can be used to add generic constraints to
the involved generic parameters:
type
Dictionary[K: Hashable; V] = concept
proc `[]`(a: Self; key: K): V
proc `[]=`(a: var Self; key: K; value: V)
each T
Note: each T is currently not implemented.
each T allows to introduce generic parameters that are not part of a
concept's generic parameter list. It is furthermore a special case to
allow for the common "every field has to fulfill property P" scenario:
type
Serializable = concept
iterator fieldPairs(x: Self): (string, each T)
proc write(x: T)
proc writeStuff[T: Serializable](x: T) =
for name, field in fieldPairs(x):
write name
write field
either orelse
Note: either orelse is currently not implemented.
In generic code it's often desirable to specialize the code in an ad-hoc manner.
system.addQuoted is an example of this:
proc addQuoted[T](dest: var string; x: T) =
when compiles(dest.add(x)):
dest.add(x)
else:
dest.add($x)
If we want to describe T with a concept we need some way to describe optional
aspects. either orelse can be used:
type
Quotable = concept
either:
proc `$`(x: Self): string
orelse:
proc add(s: var string; elem: self)
proc addQuoted[T: Quotable](s: var string; x: T) =
when compiles(s.add(x)):
s.add(x)
else:
s.add($x)
More examples
system.find
It's straight-forward:
type
Findable[T] = concept
iterator items(x: Self): T
proc `==`(a, b: T): bool
proc find(x: Findable[T]; elem: T): int =
var i = 0
for a in x:
if a == elem: return i
inc i
return -1
Sortable
Note that a declaration like
type
Sortable[T] = Indexable[T] and T is Comparable and T is Swapable
is possible but unwise. The reason is that Indexable either contains
too many procs we don't need or accessors that are slightly off as they don't
offer the right kind of mutability access.
Here is the proper definition:
type
Sortable[T] = concept
proc `[]`(a: var Self; b: int): var T
proc len(a: Self): int
proc swap(x, y: var T)
proc cmp(a, b: T): int
Concept matching
A type T matches a concept C if every proc and iterator header
H of C matches an entity E in the current scope.
The matching process is forgiving:
-
If H is a proc, E can be a proc, a func, a method, a template,
a converter or a macro. E can have more parameters than H as long
as these parameters have default values. The parameter names do not have
to match.
-
If H has the form proc p(x: Self): T then E can be a public
object field of name p and of type T.
-
If H is an iterator, E must be an iterator too, but E's parameter
names do not have to match and it can have additional default parameters.
Escape hatch
Generic routines that have at least one concept parameter are type-checked at declaration time. To disable type-checking in certain code sections an untyped block can be used:
proc sort(x: var Sortable) =
...
# damn this sort doesn't work, let's find out why:
untyped:
# no need to change 'Sortable' so that it mentions '$' for the involved
# element type!
echo x[i], " ", x[j]
EDITED 2021/03/09 self was renamed to Self and is what the experimental implementation uses.
Concept redesign
Goals:
not only at instantiation time.
debug
echoorlogstatement does not require a far reaching typeconstraint addition.
concept's implementation onsystem.compileswhich isunder-specified in Nim, very tied to the current implementation, and
finally, slow.
Non goals:
we have the escape hatch plus the fact that you can leave your
Tunderspecified.
proc needs a
cdeclcalling convention!")yet to see convincing examples of these cases. In the worst case, we could
add
enableiffor this without bloating the concept's design.macros. Having said that, since these concepts are declarative, they are
easier to process with tooling and arguably easier to process with macros
as well.
Atoms and containers
Concepts come in two forms: Atoms and containers. A container is a generic
concept like
Iterable[T], an atom always lacks any kind of genericparameter (as in
Comparable).Syntactically a concept consists of a list of
procanditeratordeclarations. There are 3 syntatic additions:
Selfis a builtin type within the concept's body stands for thecurrent concept.
eachis used to introduce a generic parameterTwithin theconcept's body that is not listed within the concept's generic
parameter list.
either orelseis used to provide basic support for optionalprocs within a concept.
We will see how these are used in the examples.
Atoms
Selfstands for the currently defined concept itself. It is used to avoida recursion,
proc cmp(a, b: Comparable): intis invalid.Containers
A container has at least one generic parameter (most often called
T). Thefirst syntactic usage of the generic parameter specifies how to infer and bind
T.Other usages of
Tare then checked to match what it was bound to.Nothing interesting happens when we use multiple generic parameters:
The usual ": Constraint" syntax can be used to add generic constraints to
the involved generic parameters:
each T
Note:
each Tis currently not implemented.each Tallows to introduce generic parameters that are not part of aconcept's generic parameter list. It is furthermore a special case to
allow for the common "every field has to fulfill property P" scenario:
either orelse
Note:
either orelseis currently not implemented.In generic code it's often desirable to specialize the code in an ad-hoc manner.
system.addQuotedis an example of this:If we want to describe
Twith a concept we need some way to describe optionalaspects.
either orelsecan be used:More examples
system.find
It's straight-forward:
Sortable
Note that a declaration like
is possible but unwise. The reason is that
Indexableeither containstoo many procs we don't need or accessors that are slightly off as they don't
offer the right kind of mutability access.
Here is the proper definition:
Concept matching
A type
Tmatches a conceptCif every proc and iterator headerHofCmatches an entityEin the current scope.The matching process is forgiving:
If
His aproc,Ecan be a proc, a func, a method, a template,a converter or a macro.
Ecan have more parameters thanHas longas these parameters have default values. The parameter names do not have
to match.
If
Hhas the formproc p(x: Self): TthenEcan be a publicobject field of name
pand of typeT.If
His an iterator,Emust be an iterator too, butE's parameternames do not have to match and it can have additional default parameters.
Escape hatch
Generic routines that have at least one concept parameter are type-checked at declaration time. To disable type-checking in certain code sections an
untypedblock can be used:EDITED 2021/03/09
selfwas renamed toSelfand is what the experimental implementation uses.