Skip to content

docs: Update concepts to cover RFC 168 #25077

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: devel
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 113 additions & 96 deletions doc/manual_experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,21 @@ to describe usage protocols that do not reveal implementation details.
Much like generics, concepts are instantiated exactly once for each tested type
and any static code included within the body is executed only once.

Concepts were overhauled with RFC #168 <https://github.com/nim-lang/RFCs/issues/168>,
so that generic code can be type-checked at declaration time rather than only at instantiation.
The redesign focuses on making concepts easier to understand and implement.
It ensures backward compatibility by allowing old code with unconstrained generic parameters to continue working.
The implementation avoids relying on system.compiles, which is under-specified,
tightly coupled to Nim’s current implementation, and slow.

The redesign does not aim to support every detail of the old concept design.
It does not support accidental or 'hacky' features (e.g., specifying calling conventions within concepts),
nor does it support complex cross-parameter constraints like comparing sizes of parameters.
Such cases can be handled separately with `enableif` without complicating the core concept design.
Finally, the redesign does not turn concepts into interfaces, as this can already be done with macros;
instead, the declarative nature of the new concepts makes them easier to process with tooling and macros.

The new style is covered below in subsections with a star (`*`) in the name.

Concept diagnostics
-------------------
Expand Down Expand Up @@ -1249,119 +1264,121 @@ object inheritance syntax involving the `of` keyword:
# matching the BidirectionalGraph concept
```

..
Converter type classes
----------------------

Concepts can also be used to convert a whole range of types to a single type or
a small set of simpler types. This is achieved with a `return` statement within
the concept body:

```nim
type
Stringable = concept x
$x is string
return $x

StringRefValue[CharType] = object
base: ptr CharType
len: int

StringRef = concept x
# the following would be an overloaded proc for cstring, string, seq and
# other user-defined types, returning either a StringRefValue[char] or
# StringRefValue[wchar]
return makeStringRefValue(x)

# the varargs param will here be converted to an array of StringRefValues
# the proc will have only two instantiations for the two character types
proc log(format: static string, varargs[StringRef])

# this proc will allow char and wchar values to be mixed in
# the same call at the cost of additional instantiations
# the varargs param will be converted to a tuple
proc log(format: static string, varargs[distinct StringRef])
```


..
VTable types
------------

Concepts allow Nim to define a great number of algorithms, using only
static polymorphism and without erasing any type information or sacrificing
any execution speed. But when polymorphic collections of objects are required,
the user must use one of the provided type erasure techniques - either common
base types or VTable types.

VTable types are represented as "fat pointers" storing a reference to an
object together with a reference to a table of procs implementing a set of
required operations (the so called vtable).
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`).

In contrast to other programming languages, the vtable in Nim is stored
externally to the object, allowing you to create multiple different vtable
views for the same object. Thus, the polymorphism in Nim is unbounded -
any type can implement an unlimited number of protocols or interfaces not
originally envisioned by the type's author.
Syntactically a concept consists of a list of proc and iterator declarations.
There are 3 syntatic additions:

Any concept type can be turned into a VTable type by using the `vtref`
or the `vtptr` compiler magics. Under the hood, these magics generate
a converter type class, which converts the regular instances of the matching
types to the corresponding VTable type.
- `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.

```nim
type
IntEnumerable = vtref Enumerable[int]
We will see how these are used in the examples.

MyObject = object
enumerables: seq[IntEnumerable]
streams: seq[OutputStream.vtref]
Atoms*
------
```nim
type
Comparable = concept # no T, an atom
proc cmp(a, b: Self): int

proc addEnumerable(o: var MyObject, e: IntEnumerable) =
o.enumerables.add e
ToStringable = concept
proc `$`(a: Self): string

proc addStream(o: var MyObject, e: OutputStream.vtref) =
o.streams.add e
```
Hashable = concept
proc hash(x: Self): int
proc `==`(x, y: Self): bool

The procs that will be included in the vtable are derived from the concept
body and include all proc calls for which all param types were specified as
concrete types. All such calls should include exactly one param of the type
matched against the concept (not necessarily in the first position), which
will be considered the value bound to the vtable.
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.

Overloads will be created for all captured procs, accepting the vtable type
in the position of the captured underlying object.
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.

Under these rules, it's possible to obtain a vtable type for a concept with
unbound type parameters or one instantiated with metatypes (type classes),
but it will include a smaller number of captured procs. A completely empty
vtable will be reported as an error.
```nim
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:
```nim
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:
```nim
type
Dictionary[K: Hashable; V] = concept
proc `[]`(a: Self; key: K): V
proc `[]=`(a: var Self; key: K; value: V)
```

The `vtref` magic produces types which can be bound to `ref` types and
the `vtptr` magic produced types bound to `ptr` types.
More examples*
--------------
**system.find**

```nim
type
Findable[T] = concept
iterator items(x: Self): T
proc `==`(a, b: T): bool

..
deepCopy
--------
`=deepCopy` is a builtin that is invoked whenever data is passed to
a `spawn`'ed proc to ensure memory safety. The programmer can override its
behaviour for a specific `ref` or `ptr` type `T`. (Later versions of the
language may weaken this restriction.)
proc find[T](x: Findable[T]; elem: T): int =
var i = 0
for a in x:
if a == elem: return i
inc i
result = -1
```

The signature has to be:
**Sortable**

```nim
proc `=deepCopy`(x: T): T
```
Note that a declaration like
```nim
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:
```nim
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
```

This mechanism will be used by most data structures that support shared memory,
like channels, to implement thread safe automatic memory management.
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 builtin `deepCopy` can even clone closures and their environments. See
the documentation of [spawn][spawn statement] for details.
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.

Dynamic arguments for bindSym
=============================
Expand Down
Loading