future manual additions for the full concept spec I'm aiming to implement
This commit is contained in:
parent
76c663f692
commit
19918ad96f
1 changed files with 361 additions and 19 deletions
|
|
@ -148,8 +148,8 @@ as `type constraints`:idx: of the generic type parameter:
|
||||||
onlyIntOrString("xy", 50) # invalid as 'T' cannot be both at the same time
|
onlyIntOrString("xy", 50) # invalid as 'T' cannot be both at the same time
|
||||||
|
|
||||||
By default, during overload resolution each named type class will bind to
|
By default, during overload resolution each named type class will bind to
|
||||||
exactly one concrete type. Here is an example taken directly from the system
|
exactly one concrete type. We call such type classes `bind once`:idx: types.
|
||||||
module to illustrate this:
|
Here is an example taken directly from the system module to illustrate this:
|
||||||
|
|
||||||
.. code-block:: nim
|
.. code-block:: nim
|
||||||
proc `==`*(x, y: tuple): bool =
|
proc `==`*(x, y: tuple): bool =
|
||||||
|
|
@ -161,7 +161,8 @@ module to illustrate this:
|
||||||
if a != b: result = false
|
if a != b: result = false
|
||||||
|
|
||||||
Alternatively, the ``distinct`` type modifier can be applied to the type class
|
Alternatively, the ``distinct`` type modifier can be applied to the type class
|
||||||
to allow each param matching the type class to bind to a different type.
|
to allow each param matching the type class to bind to a different type. Such
|
||||||
|
type classes are called `bind many`:idx: types.
|
||||||
|
|
||||||
Procs written with the implicitly generic style will often need to refer to the
|
Procs written with the implicitly generic style will often need to refer to the
|
||||||
type parameters of the matched generic type. They can be easily accessed using
|
type parameters of the matched generic type. They can be easily accessed using
|
||||||
|
|
@ -211,34 +212,375 @@ Concepts are written in the following form:
|
||||||
Comparable = concept x, y
|
Comparable = concept x, y
|
||||||
(x < y) is bool
|
(x < y) is bool
|
||||||
|
|
||||||
Container[T] = concept c
|
Stack[T] = concept s, var v
|
||||||
c.len is Ordinal
|
s.pop() is T
|
||||||
items(c) is T
|
v.push(T)
|
||||||
for value in c:
|
|
||||||
type(value) is T
|
s.len is Ordinal
|
||||||
|
|
||||||
|
for value in s:
|
||||||
|
value is T
|
||||||
|
|
||||||
The concept is a match if:
|
The concept is a match if:
|
||||||
|
|
||||||
a) all of the expressions within the body can be compiled for the tested type
|
a) all of the expressions within the body can be compiled for the tested type
|
||||||
b) all statically evaluatable boolean expressions in the body must be true
|
b) all statically evaluable boolean expressions in the body must be true
|
||||||
|
|
||||||
The identifiers following the ``concept`` keyword represent instances of the
|
The identifiers following the ``concept`` keyword represent instances of the
|
||||||
currently matched type. These instances can act both as variables of the type,
|
currently matched type. You can apply any of the standard type modifiers such
|
||||||
when used in contexts where a value is expected, and as the type itself when
|
as ``var``, ``ref``, ``ptr`` and ``static`` to denote a more specific type of
|
||||||
used in contexts where a type is expected.
|
instance. You can also apply the `type` modifier to create a named instance of
|
||||||
|
the type itself:
|
||||||
|
|
||||||
|
.. code-block:: nim
|
||||||
|
type
|
||||||
|
MyConcept = concept x, var v, ref r, ptr p, static s, type T
|
||||||
|
...
|
||||||
|
|
||||||
|
Within the concept body, types can appear in positions where ordinary values
|
||||||
|
and parameters are expected. This provides a more convenient way to check for
|
||||||
|
the presence of callable symbols with specific signatures:
|
||||||
|
|
||||||
|
.. code-block:: nim
|
||||||
|
type
|
||||||
|
OutputStream = concept var s
|
||||||
|
s.write(string)
|
||||||
|
|
||||||
|
In order to check for symbols accepting ``typedesc`` params, you must prefix
|
||||||
|
the type with an explicit ``type`` modifier. The named instance of the type,
|
||||||
|
following the ``concept`` keyword is also considered an explicit ``typedesc``
|
||||||
|
value that will be matched only as a type.
|
||||||
|
|
||||||
|
.. code-block:: nim
|
||||||
|
type
|
||||||
|
# Let's imagine a user-defined casting framework with operators
|
||||||
|
# such as `val.to(string)` and `val.to(JSonValue)`. We can test
|
||||||
|
# for these with the following concept:
|
||||||
|
MyCastables = concept x
|
||||||
|
x.to(type string)
|
||||||
|
x.to(type JSonValue)
|
||||||
|
|
||||||
|
# Let's define a couple of concepts, known from Algebra:
|
||||||
|
AdditiveMonoid* = concept x, y, type T
|
||||||
|
x + y is T
|
||||||
|
T.zero is T # require a proc such as `int.zero` or 'Position.zero'
|
||||||
|
|
||||||
|
AdditiveGroup* = concept x, y, type T
|
||||||
|
x is AdditiveMonoid
|
||||||
|
-x is T
|
||||||
|
x - y is T
|
||||||
|
|
||||||
Please note that the ``is`` operator allows one to easily verify the precise
|
Please note that the ``is`` operator allows one to easily verify the precise
|
||||||
type signatures of the required operations, but since type inference and
|
type signatures of the required operations, but since type inference and
|
||||||
default parameters are still applied in the provided block, it's also possible
|
default parameters are still applied in the concept body, it's also possible
|
||||||
to encode usage protocols that do not reveal implementation details.
|
to encode usage protocols that do not reveal implementation details.
|
||||||
|
|
||||||
Much like generics, concepts are instantiated exactly
|
Much like generics, concepts are instantiated exactly once for each tested type
|
||||||
once for each tested type and any static code included within them is also
|
and any static code included within the body is executed only once.
|
||||||
executed once.
|
|
||||||
|
|
||||||
**Hint**: Since concepts are still very rough at the edges there is a
|
|
||||||
command line switch ``--reportConceptFailures:on`` to make debugging
|
Concept diagnostics
|
||||||
concept related type failures more easy.
|
-------------------
|
||||||
|
|
||||||
|
By default, the compiler will report the matching errors in concepts only when
|
||||||
|
no other overload can be selected and a normal compilation error is produced.
|
||||||
|
When you need to understand why the compiler is not matching a particular
|
||||||
|
concept and, as a result, a wrong overload is selected, you can apply the
|
||||||
|
``explain`` pragma to either the concept body or a particular call-site.
|
||||||
|
|
||||||
|
.. code-block:: nim
|
||||||
|
type
|
||||||
|
MyConcept {.explain.} = concept ...
|
||||||
|
|
||||||
|
overloadedProc(x, y, z) {.explain.}
|
||||||
|
|
||||||
|
This will provide Hints in the compiler output either every time the concept is
|
||||||
|
not matched or only on the particular call-site.
|
||||||
|
|
||||||
|
|
||||||
|
Generic concepts and type binding rules
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
The concept types can be parametric just like the regular generic types:
|
||||||
|
|
||||||
|
.. code-block:: nim
|
||||||
|
### matrixalgo.nim
|
||||||
|
type
|
||||||
|
AnyMatrix*[R, C: static[int]; T] = concept m, var mvar, type M
|
||||||
|
M.ValueType is T
|
||||||
|
M.Rows == R
|
||||||
|
M.Cols == C
|
||||||
|
|
||||||
|
m[int, int] is T
|
||||||
|
mvar[int, int] = T
|
||||||
|
|
||||||
|
AnySquareMatrix*[N: static[int], T] = AnyMatrix[N, N, T]
|
||||||
|
|
||||||
|
proc transpose*[R, C, T](m: AnyMatrix[R, C, T]): m.type.basis[C, R, T] =
|
||||||
|
for r in 0 .. <m.R:
|
||||||
|
for c in 0 .. <m.C:
|
||||||
|
result[r, c] = m[c, r]
|
||||||
|
|
||||||
|
proc determinant*(m: AnySquareMatrix): int =
|
||||||
|
...
|
||||||
|
|
||||||
|
--------------
|
||||||
|
### matrix.nim
|
||||||
|
|
||||||
|
type
|
||||||
|
Matrix*[M, N: static[int]; T] = object
|
||||||
|
data: array[M*N, T]
|
||||||
|
|
||||||
|
proc `[]`*(m: Matrix; m, n: int): m.T =
|
||||||
|
m.data[m * m.N + n]
|
||||||
|
|
||||||
|
proc `[]=`*(m: var Matrix; m, n: int; v: m.T) =
|
||||||
|
m.data[m * m.N + n] = v
|
||||||
|
|
||||||
|
# Adapt the Matrix type to the concept's requirements
|
||||||
|
template Rows*(M: type Matrix): expr = M.M
|
||||||
|
template Cols*(M: type Matrix): expr = M.N
|
||||||
|
template ValueType*(M: type Matrix): typedesc = M.T
|
||||||
|
|
||||||
|
-------------
|
||||||
|
### usage.nim
|
||||||
|
|
||||||
|
import matrix, matrixalgo
|
||||||
|
|
||||||
|
var v: Matrix[3, 3, int]
|
||||||
|
echo v.determinant
|
||||||
|
|
||||||
|
When the concept type is matched against a concrete type, the unbound type
|
||||||
|
parameters are inferred from the body of the concept in a way that closely
|
||||||
|
resembles the way generic parameters of callable symbols are inferred on
|
||||||
|
call sites.
|
||||||
|
|
||||||
|
Unbound types can appear both as params to calls such as `s.push(T)` and
|
||||||
|
on the right-hand side of the ``is`` operator in cases such as `x.pop is T`
|
||||||
|
and `x.data is seq[T]`.
|
||||||
|
|
||||||
|
Unbound static params will be inferred from expressions involving the `==`
|
||||||
|
operator and also when types dependent on them are being matched:
|
||||||
|
|
||||||
|
.. code-block:: nim
|
||||||
|
type
|
||||||
|
MyConcept[M, N: static[int]; T] = concept x
|
||||||
|
x.foo(SquareMatrix[N, T]) is array[M, int]`
|
||||||
|
|
||||||
|
Nim may include a simple linear equation solver in the future to help us
|
||||||
|
infer static params when arithmetic is involved.
|
||||||
|
|
||||||
|
Just like in regular type classes, Nim discriminates between ``bind once``
|
||||||
|
and ``bind many`` types when matching the concept. You can add the ``distinct``
|
||||||
|
modifier to any of the otherwise inferable types to get a type that will be
|
||||||
|
matched without permanently inferring it. This may be useful when you need
|
||||||
|
to match several procs accepting the same wide class of types:
|
||||||
|
|
||||||
|
.. code-block:: nim
|
||||||
|
type
|
||||||
|
Enumerable[T] = concept e
|
||||||
|
for v in e:
|
||||||
|
v is T
|
||||||
|
|
||||||
|
type
|
||||||
|
MyConcept = concept o
|
||||||
|
# this could be inferred to a type such as Enumerable[int]
|
||||||
|
o.foo is distinct Enumerable
|
||||||
|
|
||||||
|
# this could be inferred to a different type such as Enumerable[float]
|
||||||
|
o.bar is distinct Enumerable
|
||||||
|
|
||||||
|
# it's also possible to give an alias name to a `bind many` type class
|
||||||
|
type Enum = distinct Enumerable
|
||||||
|
o.baz is Enum
|
||||||
|
|
||||||
|
On the other hand, using ``bind once`` types allows you to test for equivalent
|
||||||
|
types used in multiple signatures, without actually requiring any concrete
|
||||||
|
types, thus allowing you to encode implementation detail types:
|
||||||
|
|
||||||
|
.. code-block:: nim
|
||||||
|
type
|
||||||
|
MyConcept = concept x
|
||||||
|
type T1 = auto
|
||||||
|
x.foo(T1)
|
||||||
|
x.bar(T1) # both procs must accept the same type
|
||||||
|
|
||||||
|
type T2 = seq[SomeNumber]
|
||||||
|
x.alpha(T2)
|
||||||
|
x.omega(T2) # both procs must accept the same type
|
||||||
|
# and it must be a numeric sequence
|
||||||
|
|
||||||
|
As seen in the previous examples, you can refer to generic concepts such as
|
||||||
|
Enumerable[T] just by their short name. Much like the regular generic types,
|
||||||
|
the concept will be automatically instantiated with the bind once auto type
|
||||||
|
in the place of each missing generic param.
|
||||||
|
|
||||||
|
|
||||||
|
Concept derived values
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
All top level constants or types appearing within the concept body are
|
||||||
|
accessible through the dot operator in procs where the concept was successfully
|
||||||
|
matched to a concrete type:
|
||||||
|
|
||||||
|
.. code-block:: nim
|
||||||
|
type
|
||||||
|
DateTime = concept t1, t2, type T
|
||||||
|
const Min = T.MinDate
|
||||||
|
T.Now is T
|
||||||
|
|
||||||
|
t1 < t2 is bool
|
||||||
|
|
||||||
|
type TimeSpan = type(t1 - t2)
|
||||||
|
TimeSpan * int is TimeSpan
|
||||||
|
TimeSpan + TimeSpan is TimeSpan
|
||||||
|
|
||||||
|
t1 + TimeSpan is T
|
||||||
|
|
||||||
|
proc eventsJitter(events: Enumerable[DateTime]): float =
|
||||||
|
var
|
||||||
|
# this variable will have the inferred TimeSpan type for
|
||||||
|
# the concrete Date-like value the proc was called with:
|
||||||
|
averageInterval: DateTime.TimeSpan
|
||||||
|
|
||||||
|
deviation: float
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
Concept refinement
|
||||||
|
------------------
|
||||||
|
|
||||||
|
When the matched type within a concept is directly tested against a different
|
||||||
|
concept, we say that the outer concept is a refinement of the inner concept and
|
||||||
|
thus it is more-specific. When both concepts are matched in a call during
|
||||||
|
overload resolution, Nim will assign a higher precedence to the most specific
|
||||||
|
one. As an alternative way of defining concept refinements, you can use the
|
||||||
|
object inheritance syntax involving the ``of`` keyword:
|
||||||
|
|
||||||
|
.. code-block:: nim
|
||||||
|
type
|
||||||
|
Graph = concept g, type G of EqualyComparable, Copyable
|
||||||
|
type
|
||||||
|
VertexType = G.VertexType
|
||||||
|
EdgeType = G.EdgeType
|
||||||
|
|
||||||
|
VertexType is Copyable
|
||||||
|
EdgeType is Copyable
|
||||||
|
|
||||||
|
var
|
||||||
|
v: VertexType
|
||||||
|
e: EdgeType
|
||||||
|
|
||||||
|
IncidendeGraph = concept of Graph
|
||||||
|
# symbols such as variables and types from the refined
|
||||||
|
# concept are automatically in scope:
|
||||||
|
|
||||||
|
g.source(e) is VertexType
|
||||||
|
g.target(e) is VertexType
|
||||||
|
|
||||||
|
g.outgoingEdges(v) is Enumerable[EdgeType]
|
||||||
|
|
||||||
|
BidirectionalGraph = concept g, type G
|
||||||
|
# The following will also turn the concept into a refinement when it
|
||||||
|
# comes to overload resolution, but it doesn't provide the convenient
|
||||||
|
# symbol inheritance
|
||||||
|
g is IncidendeGraph
|
||||||
|
|
||||||
|
g.incomingEdges(G.VertexType) is Enumerable[G.EdgeType]
|
||||||
|
|
||||||
|
proc f(g: IncidendeGraph)
|
||||||
|
proc f(g: BidirectionalGraph) # this one will be preferred if we pass a type
|
||||||
|
# matching the BidirectionalGraph concept
|
||||||
|
|
||||||
|
|
||||||
|
Converter type classes
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Concepts can also be used to reduce 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:
|
||||||
|
|
||||||
|
.. code-block:: 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)
|
||||||
|
|
||||||
|
# this proc will have only two instantiations for the two character types
|
||||||
|
# the varargs param will be converted to an array of StringRefValues
|
||||||
|
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).
|
||||||
|
|
||||||
|
In contrast to other programming languages, the vtable in Nim is stored
|
||||||
|
externally to the object, allowing you to create multiple 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.
|
||||||
|
|
||||||
|
Any concept type can be turned into a VTable type by using the ``vtable``
|
||||||
|
or the ``ptrvtable`` 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.
|
||||||
|
|
||||||
|
.. code-block:: nim
|
||||||
|
type
|
||||||
|
IntEnumerable = vtable Enumerable[int]
|
||||||
|
|
||||||
|
MyObject = object
|
||||||
|
enumerables: seq[IntEnumerable]
|
||||||
|
additives: seq[AdditiveGroup.vtable]
|
||||||
|
|
||||||
|
proc addEnumerable(o: var MyObject, e: IntEnumerable) =
|
||||||
|
o.enumerables.add e
|
||||||
|
|
||||||
|
proc addAdditive(o: var MyObject, e: AdditiveGroup.vtable) =
|
||||||
|
o.additives.add e
|
||||||
|
|
||||||
|
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 inferred
|
||||||
|
successfully to concrete types. All such calls should include at least one
|
||||||
|
param of the type matched against the concept (not necessarily in the first
|
||||||
|
position). If there is more than one such param, the one appearing closest
|
||||||
|
to the ``concept`` keyword is considered the value bound to the vtable.
|
||||||
|
|
||||||
|
Overloads will be created for all captured procs, accepting the vtable value
|
||||||
|
in the position of the captured underlying object.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The ``vtable`` magic produces types, which can be bound to ``ref`` types and
|
||||||
|
the ``ptrvtable`` magic produced types bound to ``ptr`` types.
|
||||||
|
|
||||||
|
|
||||||
Symbol lookup in generics
|
Symbol lookup in generics
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue