You've picked the right moment to ask, because the user-defined type classes will be landing in the github repo in less than a week.
The notion of a type class in nimrod is closer to the C++'s notion of concepts than the Haskell's type classes. A type class merely represents a set of arbitrary requirements that must hold for a type in order to be accepted as a parameter to a generic function. Usually these will be written by the user in the following form:
type
Container[T] = generic c
c.size is ordinal
items(c) is iterator
for item in c:
type(item) is T
Copyable = generic value
var assigned = value
var copied = value.type(value)
The key thing to understand here is that the code representing the body of the type class could actually be an arbitrary piece of code that must: a) compile for the tested type and b) all static predicates appearing in the block must evaluate to true. It just happens that the is operator offers an easy way to verify the call signatures appearing in the type classes definitions in other languages.
Once you have defined a type class, you can use it like a regular type to define implicitly generic functions:
proc foo(x: Copyable, y: Container[Copyable]) =
...
or you can directly test whether certain types meet the requirements of the type class.
int is Copyable # returns true
int is Container # returns false
As an alternative to the definitions above, since Nimrod also has powerful compile-time function execution capabilities, you'll also be able to write a regular compile-time function that can accept or reject types in arbitrary fashion. There are also some more advanced features like the ability to modify the inputs of a function while matching with a type class (e.g. a type class like ConvertibleToString that checks whether such an operation is possible and applies it automatically). All of this will be documented in the manual hopefully in a week from now.
job comes first, but I am really curious about this feature. Btw. how would be the syntax to distinguish if a parameter to a proc is var, ref, ptr or by value ?
like this ?
type
MyClass = generic c
Foo(var c) is Int
I plan to rely on prefix operators accepting typedescs that will create "dummy values" of the designated type.
Something along the lines of:
type
MyClass = generic c
c.foo(@PBar, @array[int]) is int
I still haven't made my mind, which operator to pick for this purpose.
Checking for var parameters will indeed require additional support in the compiler that goes a bit beyond this "dummy value" explanation, but to the user it will be similar: @(var int) or even @ var int.
There is also another technique that is probably going to be less popular, but I'll mention it for completeness. You can use the compiles magic to create negative checks.
type
MyClass = generic c
let x = c
not compiles(foo(x))
I plan to add a few helper templates that will be able to hide such negative checks behind a higher-level interface that will be also affecting the error message you get when the type class doesn't match.
type
MyClass = generic c
should "support random access": c[5]
shouldnt "allow assignment": c[3] = 10 # if this compiles, you get an error message like "TMyType shouldn't allow assignment"
# with a highlight to the failing line in the type class.
We have a misunderstanding here: I don't propose to get rid of the list of general expressions that need to match. Instead I'm proposing for serializable the following:
type Serializable = generic value
value is object or tuple
for field in fields(value):
proc serialize(s: var Stream, x: type(field))
In other words routine headers act like a predicate in a 'generic' context. But perhaps this looks weird too. ;-)
Yes, I fail to see how mixing imperative code and declarations is more "pretty" or "nimrod-like", but if that's all you want, then this is a minor feature request and I can support it, but please note that since we are still using the compilation method, my way of doing it will still be possible (we can't forbit it) and many people will use it anyway because it's more concise.
If we are arguing about something here, it's what should be the recommended way in the manual and then I would still reccomend my version. What if the serialize operation is implemented with a template for some type instead of a proc? If we don't match the template, then the type class is overly restrictive and if we match it, then the code becomes a bit hypocritical , doesn't it?
What if the serialize operation is implemented with a template for some type instead of a proc?
good point. And I think zaharys version of Serializable looks more consistent in style.
Well often you only want to specify a list of operations and then it is:
type
ArrayLike[T] = generic a
proc len(x: a): int
(proc `[]` (x: var a, idx: int): var T) or (
proc `[]` (x: a, idx: int): T
proc `[]=` (x: var a, idx: int, value: T))
VS
type
ArrayLike = generic a
for i in 0 .. < a.len:
a[i] = a[i]
The second is certainly much more concise and flexible. It's however not declarative. This is just system.compiles attached to a type for overloading resolution purposes right?
Yes, the main motivation for introducing type classes in generic programming is to provide overload resolution and to improve the error messages in generic code (you fail early with a message produced from the type class library instead of late with a compiler message featuring multiple "instantiated from here" steps).
It's up to the type class library to pick its level of declarativeness. Again, I could have written your code in even more "declarative" fashion:
type
ArrayLike[T] = generic a, var va
a.len is int
(should "support direct-access indexing operator": va[int] is var T) or
should "has a read-write pair of indexing operators":
v[int] is T
va[int] = T
The error message will be "Type TFoo should support direct-access indexing operator or it should has a read-write pair of indexing operators".
Furthermore, since this is regular Nimrod code, it can be further abstracted away with templates if needed. It's also possible to avoid the funny indentation if you instead assign the results of the should expressions to constants:
const a = should ...
const b = should ...
a or b
will I still need to mixin names used in a generic type class, because they are unknown at that moment :
type
GermanIterator[T] = generic C
derNaechsteBitte(var C, var bool): T
proc loop[T](it: var GermanIterator[T]) =
mixin derNaechsteBitte # needed to compile
var proceed : bool
var el = it.derNaechsteBitte(proceed)
while proceed :
echo el
el = it.derNaechsteBitte(proceed)
Sorry for disappearing. I was very busy in the last two weeks, but now things should get back to normal (or should I say abnormal) with me having more time for nimrod on my hands.
To answer the last question, I don't have plans to modify the symbol look-up rules for generic code, but indeed only time will tell if adding something like ADL (argument-dependent lookup) will be helpful.