-
Notifications
You must be signed in to change notification settings - Fork 23
Description
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
debugechoorlogstatement does not require a far reaching type
constraint addition. - Old code with unconstrainted generic parameters keeps to work.
- Do not base
concept's implementation onsystem.compileswhich 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 yourT
underspecified. - Support accidentical features. ("Look! With this hack I can specify that the
proc needs acdeclcalling 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
addenableiffor 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:
Selfis a builtin type within the concept's body stands for the
current concept.eachis used to introduce a generic parameterTwithin the
concept's body that is not listed within the concept's generic
parameter list.either orelseis 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): intNothing 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 fieldeither 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 -1Sortable
Note that a declaration like
type
Sortable[T] = Indexable[T] and T is Comparable and T is Swapableis 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): intConcept 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
His aproc,Ecan be a proc, a func, a method, a template,
a converter or a macro.Ecan have more parameters thanHas long
as these parameters have default values. The parameter names do not have
to match. -
If
Hhas the formproc p(x: Self): TthenEcan be a public
object field of namepand of typeT. -
If
His an iterator,Emust be an iterator too, butE'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.