Recently, a thread starting with a simple question went OT and became a different exchange starting here. The short version:
"Methods (inheritance based dispatch) and concepts (user defined type classes with VTable based dispatch) are two solutions to the same problem and we don't really need both."
"But wait, there's a difference between the two: with inheritance, the author of a derived type explicitly refers to the base type. That's probably a conscious commitment to a contract of some kind. With a concept, the definition of a matching type doesn't even mention the concept. So the match could be coincidence and there's no commitment to a behavioral contract."
Two questions about this:
The latter could look something like this:
type
MyConcept = explicit concept m
# the optional "explicit" means: only concrete types defined with "satisfies"
# match.
...
MyType = object of BaseType satisfies MyConcept
# the optional "satisfies" means: I am consciously claiming to be a MyConcept
# and I satisfy its contract.
...
If this makes any sense: should it be part of the language spec or implemented as a macro?
I consider this possibility a very useful even without additional checks (those in the concept body), like of existence of procedures defined for the type with stated signatures and the like, exactly because the programmer explicitly declares the purpose of his type and what can be done with, what functionality is really supported by the type. I think this explicit promise/contract is the main point for interfaces in Delphi, Java, PHP, C#, and signature checking is additional to that.
I'll try to make it more visual:
type Browser = concept type b
proc goTo(self: b, url: string)
proc back(self: b)
proc forward(self: b)
proc makeSomeStuffWithABrowser(br: Browser) = br.goTo("http://www.nim-lang.org/")
# creating a concrete type satisfying the concept
type MySimpleBrowser = distinct int # whatever it would be
proc goTo(self: MySimpleBrowser, url: string) = echo "going to " & url & " with MySimpleBrowser"
proc back(self: MySimpleBrowser) = discard
proc forward(self: MySimpleBrowser) = discard
# using it
var sb: MySimpleBrowser
makeSomeStuffWithABrowser(sb) # it works, well; it should
# here we don't want to define another Browser, just an unrelated type; maybe in another module
type FileManager = distinct int
proc goTo(self: FileManager, path: string) = echo "going to " & path & " with X"
proc back(self: FileManager) = discard
proc forward(self: FileManager) = discard
var fm: FileManager
# and here by accident...
makeSomeStuffWithABrowser(fm) # works too :(
Lando's suggestion is perfect in my view, if not to restrict it to object type - it can fit equally well any types, as concepts do. It can be seen like a type class (like e.g. SomeReal* = float|float32|float64), but extendable and with inverse binding: created empty and populated with concrete types at their definition site. Like:
type X = explicit concept c
# ...
# X here matches no concrete types, regardless of checks in the concept body
type Y = distinct int satisfies X # conditions in X concept body are checked,
# compile-time error, if not satisfied;
# Y is added to the list of implementations for X
type Z = distinct int satisfies X
proc p(x: X) = discard
# here the same as p(x: Y|Z)
Regarding wording, explicit concept and satisfies seem to be not worse than usual interface and implements, especially taking into account distinctions; just for people coming from those languages those terms may be more familiar; maybe explicit concept will be easier to understand to others.
Regarding implementation, macro vs. built-in: I wanted to implement such a thing some (long) time ago via macros and concepts and was faced then with some VM limitations; maybe they were eliminated from then. The intended syntax was (for simplicity):
type X = ...
implements X, I
"Type T accidentically implemented the interface I. Then a variable of type T was passed to something accepting I accidentically resulting in a hard to find bug."
Does not seem to be a realistic problem. I can see why somebody might want to annotate it explicitly just for the clarity but let's not pretend it will prevent bugs. For that you'd need hard data from the real world.
Neither a feature or a bug. :)
These are just different ways of abstraction, each of which can be preferrable in some situation. I like concepts (that is, implicit ones, as they are) too, I'd not want them just to be replaced with interfaces (explicit concepts), the proposition is of having both ways. The purpose of interfaces is to express programmer's intention; not what concepts are intented to.
About Go's interfaces: I don't know this language, I just heard that something very different is called by interfaces in it, just sharing the same word; probably it's something nearer to concepts; better not to compare things as respective just because of wording, this is why I've mentioned explicitly Delphi (Object Pascal), Java, PHP and C# (probably there are more) as having this feature.
Still there is some difference between interfaces in those languages and explicit concepts, as proposed for Nim: those are based on dynamic dispatch; this not what really needed, procs can be instantiated statically per concrete type, the same as with implicit concepts (as now). So they are still the same concepts in all respects, except:
Just in case I didn't make that clear enough: explicit and satisfies would be optional, I don't want to restrict concepts or take anything away from them in any way.
Concepts in C++ was designed to be used without explicitly stating the interface. I thing golang interfaces work the same way.
Exactly, and interfaces were designed in other languages to be explicitly implemented, but the golang people don't give a damn about that, so maybe we can take some liberties, too :). But seriously, if "explicit concept" really is a bad name, one could look at it as "interface on steroids" and invent a better one.
@Araq: Making as much author intention as possible checkable by the compiler is seen as a good thing by many people. Since you don't seem to think it's important with concepts, I won't waste people's time with a lengthy discussion. Would you be ok with macro packages in the standard lib implementing structural language extensions like "explicit concept", "class" or "interface"? I understand the "We keep the language core lean and mean, implement the high level stuff you need with meta-programming" approach, but I'm afraid that will keep tons of people away from Nim if they don't get semi-official default implementations of useful things they already know.
Would you be ok with macro packages in the standard lib implementing structural language extensions like "explicit concept", "class" or "interface"?
Yes of course, but its implementation is a one liner iirc.
but its implementation is a one liner iirc.
That's ok, these "language extension" packages would be for people who don't know how straight forward things can be implemented because they are no macro wizards, like the vast majority of programmers. Once they look at the source, they will maybe learn. Could serve as an ad-campaign for Nim's meta-programming features.
@LeuGim
Regarding implementation, macro vs. built-in: I wanted to implement such a thing some (long) time ago via macros and concepts and was faced then with some VM limitations; maybe they were eliminated from then.
Would you be interested in checking that?
Yes, seems to be mostly fixed. That time I could not create (or populate) dynamically allocated values at compiletime, like sequenses, tables, ptr/ref'ed values. As I've just tested, now sequenses, tables and strings work as expected (pointers though seem to not work, silently; probably just all unsafe features are disallowed, like yet casts and FFI).
The idea to begin with then was to create an extendable list of association of interfaces to types, implementing them (interface#1 => (type#1, type#2, ...), ...), then to append items to it in implements macro, and in a concept's body to check that the type tested is in the list for that concept.
Then probably another (more complex) macro (interface) had to be added, to create concepts with such checks inside, including for super-types of the checked type, given as a block argument a usual concept.
And another way that comes to mind is to use static parameters for types, adding them programmatically. A sketch (compiles):
type
# an interface declaration, as should be created via the `interface` macro
Intf1 = concept o
"Intf1" in o.interfaces
# initial concepts body here...
# 2 type declarations, again as should be created by a macro (kind of `implements`),
# given to it a declaration like:
# type O = object
# x: int
# and the interface name "Intf1"
O_decl[interfaces: static[seq[string]]] = object
x: int
O = O_decl[@["Intf1"]]
# type information should be used instead of names (strings)
#macro implements(
proc p(o: Intf1) = echo "ok"
var o: O
p o # -> "ok"
# this shouldn't compile (and doesn't)
#type O2 = O_decl[@[]]
#var o2: O2
#p o2 # won't match
@LeuGim It seems to me you made a type factory, actually. That's not something I would like to see, actually. Especially considering the fact I can't add concepts to types already in existence which is, by far, the greatest advantage of concepts over inheritance/interfaces.
I tried something like this:
import sequtils
import macros
type
MyCon = concept type T
# line 7:
MyCon_reprs.anyIt(it.sameType(T.getType))
var MyCon_reprs {.compileTime.} = newSeq[NimNode]()
# there is no reliable comparator for NimSym, so use NimNode
proc shout(i: MyCon) =
echo "I am here!"
static:
MyCon_reprs.add(int.getType)
MyCon_reprs.add(float.getType)
echo int.getType in MyCon_Reprs # false
for el in MyCon_Reprs:
echo el, " == ", int.getType, " is ", el.sameType(int.getType)
# prints at compile-time:
# int == int is true
# float == int is false
shout(5) # XYZ.nim(7, 16) MyCon: cannot evaluate at compile time: result112414
Which suggests me concepts aren't really integrated into the whole compile-time environment all that well...